C++学习笔记22-类模板与数组类封装案例

22.0 前言

之前已学习了函数模板的相关知识,接下来学习类模板的相关知识。


22.1 类模板语法

类模板作用:

  • 建议一个通用类,类中的成员数据类型可以不具体指定,用一个虚拟的类型来代表。

语法:

template<typename T,typename U,typename V,...> //类中需要几个数据类型就写几个
类的定义

//类的实例化
类名<数据类型1,数据类型2,数据类型3,...> 变量名(参数)  //在实例化的时候需要将模板的数据类型参数列表依次指定。

解释:

  • template – 声明创建模板。
  • typename – 表明其后面的符号是一种数据类型,可以用class代替。
  • T,U,V – 通用的数据类型,名称可以替换,通常为大写字母。
  • 注意:类模板在调用时,参数列表不可省略不写

示例:

#include<iostream>
using namespace std;
template<class NameType,class AgeType>
class Person1
{
    
    
public:
	Person1(NameType name, AgeType age)
	{
    
    
		this->name = name;
		this->age = age;
	}
	NameType name;
	AgeType age;
	void showPerson()
	{
    
    
		cout << "姓名 : " << this->name << endl;
		cout << "年龄 : " << this->age << endl;
	}
};
void test1()
{
    
    
	Person1<string, int> p("张三", 60);
	p.showPerson();
}

int main()
{
    
    
	test1();
	system("pause");
	return 0;
}

22.2 类模板和函数模板区别

类模板与函数模板的区别主要有两点:

  1. 类模板没有自动类型推导的使用方式。
  2. 类模板在函数参数列表中可以有默认参数。

示例:

#include<iostream>
#include<string>
using namespace std;

template<class NameType,class AgeType>
class Person2
{
    
    
public:
	Person2(NameType name,AgeType age)
	{
    
    
		this->name = name;
		this->age = age;
	}
	void showPerson()
	{
    
    
		cout << "name : " << this->name << endl;
		cout << "age : " << this->age << endl;
	}
	NameType name;
	AgeType age;
};

template<class NameType, class AgeType = int >
class Person2_02
{
    
    
public:
	Person2_02(NameType name, AgeType age)
	{
    
    
		this->name = name;
		this->age = age;
	}
	void showPerson()
	{
    
    
		cout << "name : " << this->name << endl;
		cout << "age : " << this->age << endl;
	}
	NameType name;
	AgeType age;
};

void test2()
{
    
    
	//Person2 p("孙悟空", 1000);  无法用自动类型推导
	Person2<string, int> p("孙悟空", 1000);  //显示指定是可以的
	p.showPerson();

	Person2_02<string> p2("猪八戒", 900.3);  //有默认参数,是默认的 
	p2.showPerson();

	//用法和函数的默认参数是相同的,也要遵循从右往左的规则
	Person2_02<string,double> p3("沙僧", 800.5);  
	p3.showPerson();
}

int main()
{
    
    
	test2();
	system("pause");
	return 0;
}

22.3 类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
#include<iostream>
using namespace std;

class Person3_1
{
    
    
public:
	void showPerson1()
	{
    
    
		cout << "Person1 show" << endl;
	}
};

class Person3_2
{
    
    
public:
	void showPerson2()
	{
    
    
		cout << "Person2 show" << endl;
	}
};

template<class T>
class MyClass3
{
    
    
public:
	T obj;
	//成员函数
	void func1()
	{
    
    
		obj.showPerson1();
	}
	void func2()
	{
    
    
		obj.showPerson2();
	}
	//这里showPerson1()和showPerson2()是两个不同类下的成员函数,但是这里用一个变量调用,从逻辑上是说不过去的
	//但是函数模板是没问题的,因为函数模板不会在一开始就创建这两个函数,只有在调用的时候才创建
};

void test3_01()
{
    
    
	MyClass3<Person3_1> p;
	p.func1();      //因为指明了T是Person3_1,所以可以调用
	//p.func2();	//因为类型不对,所以不能调用
}
int main()
{
    
    
	test3_01();
	system("pause");
	return 0;
}

22.4 类模板对象做函数参数(有查看数据类型语法小知识)

学习目标:

  • 类模板实例化出的对象,向函数传参的方式。

一共有三种传入方式:

  1. 指定传入的类型 ——直接显示对象的数据类型
  2. 参数模板化   ——将对象中的参数变为模板进行传递
  3. 整个类模板化  ——将这个对象类型模板化后进行传递

示例:

#include<iostream>
#include<string>
using namespace std;

template<class T1,class T2>
class Person4
{
    
    
public:
	Person4(T1 name, T2 age)
	{
    
    
		this->name = name;
		this->age = age;
	}
	void showPerson()
	{
    
    
		cout << "名字是: " << name << endl;
		cout << "年龄是: " << age << endl;
	}
	T1 name;
	T2 age;
};
//1.
void printPerson1(Person4<string,int>& p)  //一般用这种,因为这样代码安全,不容易出错。
{
    
    
	p.showPerson();
}

//2.
template<class T1,class T2>
void printPerson2(Person4<T1, T2>& p)
{
    
    
	cout << "T1 : " << typeid(T1).name() << endl;  //这是看数据类型的方法,可以查看类型或者变量的数据类型.
	cout << "T2 : " << typeid(T2).name() << endl;
	p.showPerson();
}

//3.
template<class T>
void printPerson3(T& p)
{
    
    
	cout << typeid(T).name() << endl;
	p.showPerson();
}

void test4_01()
{
    
    
	//1.指定传入类型
	Person4<string, int> p1("孙悟空", 100);
	printPerson1(p1);

	//2.参数模板化
	Person4<string, int> p2("猪八戒", 90);
	printPerson2(p2);	

	//3.整个类模板化
	Person4<string, int> p3("沙僧", 30);
	printPerson3(p3);
}
int main()
{
    
    
	test4_01();
	system("pause");
	return 0;
}

查看数据类型语法:

typeid(变量/数据类型).name()

总结:

  • 通过类模板创建的对象,可以有三种方式向函数进行传参。
  • 使用最多最广泛的是第一种,直接指定传入的类型。

22.5 类模板与继承

当类模板碰到继承时,需要注意以下几点:

  • 当子类继承的父类是一个类模板时,在定义定义的时候,要指定出父类中T的类型,语法为:class Son:继承方式 Base<数据类型>
  • 如果不指定,编译器无法给子类分配内存。
  • 如果想灵活指定出父类中T的类型,子类也需要变为类模板。

示例:

#include<iostream>
using namespace std;
template<class T>
class Base
{
    
    
public:
	Base()
	{
    
    
		cout << "T类型:" << typeid(T).name() << endl;
	}
	T m;
};

class Son :public Base<int>       //子类是普通函数,不指定数据类型,编译器无法分配内存.
{
    
    
public:
};

//如果想灵活指定父类T的类型,子类也需要变类模板
template<class T1, class T2>
class Son2 :public Base<T1>   //根据传递规则可知,此时父类中的数据类型T就是现在的数据类型T1
{
    
    
public:
	Son2()
	{
    
    
		cout << "T1类型:" << typeid(T1).name() << endl;
		cout << "T2类型:" << typeid(T2).name() << endl;
	}
	T2 obj;
};
void test5_01()
{
    
    
	Son s1;
	Son2<double,char> s2; //T1为int ,T2为char
}
int main()
{
    
    
	test5_01();
	system("pause");
	return 0;
}

22.6 类模板成员函数类外实现

语法:

类内定义
template<class T1,class T2,...>
//类内
函数声明;

//类外
template<class T1,class T2,...>
作用域<T1, T2,...>::函数名(){
    
    }

总结:
类模板中成员函数类外实现时,需要加上模板参数列表(无论成员函数是否使用了模板参数,都需要加,可以记为固定写法)。

示例:

#include<iostream>
using namespace std;

template<class T1,class T2>
class Person6 
{
    
    
public:
	Person6(T1 name, T2 age);  //类内声明
	//{
    
    
	//	this->name = name;
	//	this->age = age;
	//}
	void showPerson();
	//{
    
    
	//	cout << "姓名:" << name << endl;
	//	cout << "年龄:" << age << endl;
	//}
	T1 name;
	T2 age;
};

template<class T1,class T2>
Person6<T1, T2>::Person6(T1 name, T2 age) 
{
    
    
	this->name = name;
	this->age = age;
}
template<class T1, class T2>
void Person6<T1, T2>::showPerson()
{
    
    
	cout << "姓名:" << name << endl;
	cout << "年龄:" << age << endl;
}

void test6()
{
    
    
	Person6<string,int> p1("孙悟空",20);
	p1.showPerson();
}
int main()
{
    
    
	test6();
	system("pause");
	return 0;
}

22.7 类模板分文件编写

问题:

常规的分文件编写,是在.h头文件中编写类的成员函数声明,在.cpp源文件中编写类的成员函数定义。
这样写错在的问题就是——因为类中的成员函数不会在最开始就被编译器创建。当你的执行文件包含了头文件时,编译器在头文件中看到了成员函数的声明,编译不会出错,但是编译器不会去相应的.cpp文件中找这些成员函数的定义,就导致这些成员函数的定义没有被包含到执行文件中,所以调用的时候,编译器找不到声明对应的定义,就会无法解析命令。

解决

  • 方式1:直接包含.cpp源文件(一般不常用)
  • 方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制(一般都将其创建到头文件中)。

示例(方式一很简单,这里只展示方式二):

创建person.hpp头文件,编写

#pragma once
#include<iostream>
using namespace std;
template<class T1, class T2>
class Person7
{
    
    
public:
	Person7(T1 name, T2 age);
	void showPerson();
	T1 name;
	T2 age;
};

template<class T1, class T2>
Person7<T1, T2>::Person7(T1 name, T2 age)
{
    
    
	this->name = name;
	this->age = age;
}
template<class T1, class T2>
void Person7<T1, T2>::showPerson()
{
    
    
	cout << "姓名: " << name << endl;
	cout << "年龄: " << age << endl;
}

创建执行文件,编写

#include"person.hpp"
#include<string>
//类模板分文件编写问题及解决

void test7()
{
    
    
	Person7<string, int> p1("孙悟空", 1000);
	p1.showPerson();
}
int main()
{
    
    
	test7();
	system("pause");
	return 0;
}

22.8 类模板与友元


类模板配合友元函数的类内和类外实现:
  • 全局函数类内实现 —— 直接在类内声明友元即可。
  • 全局函数类外实现 —— 需要提前让编译器知道全局函数的存在。
#include<iostream>
using namespace std;

//通过全局函数来打印Person8的信息

template<class T1,class T2>
class Person8;
//类外实现
// 
//对于普通的函数和类都是一开始就由编译器创建,因为这里只声明了类而没有定义,所以会报错.
//因为函数模板同样不是一开始就被创建,而是在调用的时候才创建,所以这里不用担心类的声明和定义顺序
//因为在调用此函数模板的时候,类模板的相关定义已经被创建完毕.
template<class T1, class T2>    //函数模板的实现
void printPerson2(Person8<T1, T2> p)
{
    
    
	cout << "姓名 : " << p.name << endl;
	cout << "年龄 : " << p.age << endl;
}

template<class T1,class T2>
class Person8
{
    
    
	//全局函数 类内实现    据说只能在vs上通过
	friend void printPerson(Person8<T1, T2> p)
	{
    
    
		cout << "姓名 : " << p.name << endl;
		cout << "年龄 : " << p.age << endl;
	}
	//加空模板参数列表
	//如果全局函数是类外实现,需要让编译器知道这个函数的存在
	friend void printPerson2<>(Person8<T1, T2> p);  //声明为函数模板

public:
	Person8(T1 name, T2 age) 
	{
    
    
		this->name = name;
		this->age = age;
	}
private:
	T1 name;
	T2 age;
};

void test01()
{
    
    
	Person8<string, int> p1("汤姆", 20);
	printPerson(p1);
	printPerson2(p1);
}
int main()
{
    
    
	test01();
	system("pause");
	return 0;
}

全局函数类外实现还是很复杂的。
注意:
printPerson2函数中调用了类的成员,而此类在这个函数之前只有声明,没有定义。这对于普通的类来说是不能编译通过的,可以参考 类的友元以及类的定义和声明顺序问题
但是对于类模板和函数模板是准许的,因为函数模板同样不是一开始就被创建,而是在调用的时候才创建,类的模板在调用此函数模板前已经被创建完毕,所以这里不用担心类的声明和定义顺序。


22.9 类模板案例-数组类封装的需求分析

案例描述:实现一个通用的数组类,要求如下:

  • 可以对内置数据类型以及自定义数据类型的数据进行存储
  • 将数组中的数据存储到堆区
  • 构造函数中可以传入数组的容量
  • 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
  • 提供尾插法和尾删法对数组中的数据进行增加和删除
  • 可以通过下标的方式访问数组中的元素
  • 可以获取数组中当前元素个数和数组的容量

示例:
创建头文件: MyArray.hpp

#pragma once //防止重复包含
#include<iostream>
using namespace std;

template<class T>
class MyArray
{
    
    
public:
	//有参构造,参数是容量
	MyArray(int capacity)
	{
    
    
		cout << "MyArray有参构造调用" << endl;
		this->capacity = capacity;
		this->size = 0;
		this->pAddress = new T[this->capacity]; //capacity表示开辟的数组中的元素个数
	}

	//拷贝构造
	MyArray(const MyArray& arr)
	{
    
    
		cout << "MyArray拷贝构造调用" << endl;
		//浅拷贝
		this->capacity = arr.capacity;
		this->size = arr.size;
		//深拷贝
		this->pAddress = new T[arr.capacity];
		for (int i = 0; i < this->size; i++)
		{
    
    
			this->pAddress[i] = arr.pAddress[i];
		}
	}

	MyArray& operator=(const MyArray& arr)
	{
    
    
		cout << "MyArray的operator=构造调用" << endl;
		//判断原来堆区是否有数据
		if (this->pAddress != NULL)
		{
    
    
			delete[] this->pAddress;
			this->capacity = 0;
			this->size = 0;
		}
		//深拷贝
		this->capacity = arr.capacity;
		this->size = arr.size;
		this->pAddress = new T[arr.capacity];
		for (int i = 0; i < this->size; i++)
		{
    
    
			this->pAddress[i] = arr.pAddress[i];
		}
		return *this;
	}

	//尾插法
	void Push_Back(const T& value)
	{
    
    
		if (this->size == this->capacity) 
		{
    
    
			return;
		}
		this->pAddress[this->size] = value;
		this->size++;
	}
	//尾删法
	void Pop_Back()
	{
    
    
		//让用户访问不到删除最后一个元素,即为尾删,逻辑删除
		if (this->size == 0)
		{
    
    
			return;
		}
		this->size--;
	}
	
	//通过下标方式访问数组的元素 ,如果想让这个值作为左值,需要用引用类型,否则不会改变原数组元素. arr[0]=100
	T& operator[](int index)
	{
    
    
		return this->pAddress[index];
	}

	//返回数组的容量
	int getCapacity()
	{
    
    
		return this->capacity;
	}
	int getSize()
	{
    
    
		return this->size;
	}

	//析构函数
	~MyArray()
	{
    
    
		if (this->pAddress != 0)
		{
    
    
			cout << "MyArray析构函数调用" << endl;
			delete[] this->pAddress; //释放堆区数组
			this->pAddress = 0;
		}
	}
private:
	T* pAddress;    //指向堆区开辟的真实数组

	int capacity; //数组容量

	int size; //数组大小
};

创建执行文件:

#include<iostream>
using namespace std;
#include"MyArray.hpp"

void printArray(MyArray<int>& arr)
{
    
    
	for (int i = 0; i < arr.getSize(); i++)
	{
    
    
		cout << arr[i] << endl;   //索引
	}
}
void test9_01()
{
    
    
	MyArray<int> arr1(10);
	MyArray<int> arr2(arr1);
	MyArray<int> arr3(100);
	arr3 = arr1;
}

void test9_02() 
{
    
    
	MyArray<int> arr1(10);
	for (int i = 0; i < 6; i++)
	{
    
    
		//尾插法插入数据
		arr1.Push_Back(i);
	}
	cout << "arr1的打印输出为: " << endl;
	printArray(arr1);

	cout << "arr1的容量为: " << arr1.getCapacity() << endl;
	cout << "arr1的大小为: " << arr1.getSize() << endl;

	MyArray<int> arr2(arr1);
	cout << "arr2的打印输出为: " << endl;
	printArray(arr2);
	arr2.Pop_Back();
	cout << "尾删后\narr2的打印输出为: " << endl;
	printArray(arr2);
}

//测试自定义数据类型
class Person9
{
    
    
public:
	Person9() {
    
    };   //必须写无参构造,向堆区开辟内存的时候是没有参数的,如果不写无参构造函数,会报错.
	Person9(string name, int age)
	{
    
    
		this->name = name;
		this->age = age;
	}
	string name;
	int age;
};
void printArray(MyArray<Person9>& arr)   //函数重载
{
    
    
	for (int i = 0; i < arr.getSize(); i++)
	{
    
    
		cout << "姓名: " << arr[i].name << "  年龄: " << arr[i].age << endl;
	}
}
void test9_03()
{
    
    
	//在向堆区开辟内存的时候如果有有参构造函数,就不会调用无参构造函数,而这里又没有传入参数,那么就无法开辟内存而报错
	MyArray<Person9> arr(10); 
	Person9 p1("孙悟空", 999);
	Person9 p2("猪八戒", 900);
	Person9 p3("沙僧", 850);
	Person9 p4("赵云", 550);
	Person9 p5("关羽", 650);

	//数据插入
	arr.Push_Back(p1);
	arr.Push_Back(p2);
	arr.Push_Back(p3);
	arr.Push_Back(p4);
	arr.Push_Back(p5);

	printArray(arr);

	//输出容量
	cout << "arr的容量为: " << arr.getCapacity() << endl;
	//输出大小
	cout << "arr的大小为: " << arr.getSize() << endl;
}
int main()
{
    
    
	test9_01();
	test9_02();
	test9_03();
	system("pause");
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_49030008/article/details/123569976