全面总结C++类模板使用的基础知识

简介: 全面总结C++类模板使用的基础知识

类模板


       建立一个通用类,类中的成员数据类型可以不具体确定,先用一个虚拟的类型来代表


语法:

template<typename T>

创建类


示例:


template<class T1,class T2>
class Person
{
public:
  T1 name;
  T2 age;
};


       先使用template关键字声明类模板,接着创建Person类,属性name和age用虚拟类型T1和T2声明,这就是最简单的类模板定义。


类模板与函数模板的区别

区别一:类模板没有自动类型推导方式

区别二:类模板在模板参数列表中可以有默认参数

注意事项:有默认参数的条件是参数列表的末尾需要指定数据类型;

由于没有自动类型推导方式,所以使用的时候不能省略<>


示例:


template<class T1, class T2=int>
class Person
{
public:
  T1 name;
  T2 age;
  Person(T1 name, T2 age)
  {
  this->age = age;
  this->name = name;
  }
  void showInfo()
  {
  cout << "姓名:" << name << " 年龄:" << age << endl;
  }
};
void test02()
{
  //Person p("叶落秋白", 18);报错,缺少对应的参数列表
  Person<string,int>p1("叶落秋白", 18);
  Person<string>p2("叶落秋白", 18);
  p.showInfo();
}


       可以看到第一行代码T2的后面指定了类型为整型,所以创建类模板对象的时候可以省略T2数据类型的指定,但是切记 :在不给参数列表最后一个虚拟类型指定数据类型的情况下是无法做到参数列表含有默认参数的。


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

       类模板中的成员函数并不是一开始就创建的,只有在调用的时候才会被调用。这个知识点在下面的内容的理解上占据着重要作用。


代码测试:


class A
{
public:
  void showInfo()
  {
  cout << "调用A类的成员函数" << endl;
  }
};
class B
{
public:
  void showInfo()
  {
  cout << "调用B类的成员函数" << endl;
  }
};
template<class T>
class MyStudy
{
public:
  T pre;
  //类模板中的成员函数
  void show()
  {
  pre.showInfo();
  }
};
void test03()
{
  MyStudy<A>* S = new MyStudy<A>();//创建A类属性的类模板对象S
  MyStudy<B>* D = new MyStudy<B>();//创建B类属性的类模板对象D
  S->show();
  D->show();
  delete S;//释放堆区指针
  delete D;
  S = NULL;
  D = NULL;
}


       由于成员属性pre的类型未指定是A类还是B类,所以编译器并不会创建成员函数,无法识别showInfo函数是哪一个类的成员函数。只有我们创建的时候给类模板对象指针指定T类型,这时候调用show方法编译器才会创建成员函数,实现对应的功能。


运行效果:



类模板对象做函数参数

三个形式:


指定传入类型


参数模板化


整个类模板化


代码演示:


template<class T1,class T2=int>
class Per
{
public:
  T1 name;
  T2 height;
  Per(T1 name, T2 height)
  {
  this->name = name;
  this->height = height;
  }
  void showInfo()
  {
  cout << "姓名:" << name << " 身高:" << height << endl;
  }
};
//1、指定传入类型
void printInfo(Per<string, int>& p)
{
  p.showInfo();
}
//2、参数模板化
template<class T1,class T2>
void printInfo1(Per<T1, T2>& p)
{
  p.showInfo();
  cout << "T1的类型为:" << typeid(T1).name() << endl;
  cout << "T2的类型为:" << typeid(T2).name() << endl;
}
//3、整个类模板化
template<class t>
void printInfo2(t & p)
{
  p.showInfo();
  cout << "t的类型为:" << typeid(t).name() << endl;
}
void test04()
{
  Per<string> p("叶落秋白", 183);
  printInfo(p);
}
void test4()
{
  Per<string> p("微凉秋意", 184);
  printInfo1(p);
}
void test4a()
{
  Per<string>p("落叶归根",185);
  printInfo2(p);
}


       小结:第一种指定传入类型的方式最为常用,查看自动推导类型可以调用typeid(虚拟类型).name()函数。其实string类型原名特别长,调用typeid函数的时候可以查看,另外也可以直接查看整个类模板化时虚拟类型t的数据类型。


运行效果:



类模板与继承


       类模板的继承不同于普通类的继承。这是因为类模板的属性数据类型事先并未指定,所以子类无法分配内存空间。


要点:


直接继承会提示缺少基类类的参数列表,需要继承的时候在基类后面指定<数据类型>

如果想要灵活指定父类中的T数据类型,子类也要变成类模板


代码示例:


template<class T>
class Base
{
public:
  Base()
  {
  cout << "此时父类T的数据类型是:" << typeid(T).name() << endl;
  }
  T m;
};
//class Son :public Base<int>//这里可以直接指定,不过为了灵活指定,将子类变为类


模板


//子类变成类模板
template<class T1,class T2>
class Son :public Base<T2>
{
public:
  T1 n;
  Son() {
  cout << "T1的数据类型为:" << typeid(T1).name()<< endl;
  cout << "T2的数据类型为:" << typeid(T2).name()<< endl;
  }
};
void test05()
{
  Son<int,char> s;
}


       将子类变为类模板的时候可以自由添加子类的属性,这里子类Son的属性是T1类型的n,当然继承的T2属性的m也存在。在继承的时候将T2作为父类的虚拟类型,那么创建子类对象的时候指定的数据类型char就会先传给T2,T2再传给父类的T,那么就完成了灵活指定继承数据类型的工作


       创建子类对象是会自动调用父类构造,那么就能调用事先设置好的typeid函数来查看由子类指定的父类的虚拟数据类型的具体类型。


运行效果:



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

       成员函数类外实现需要在加作用域的前提下再加类模板的参数列表


代码演示:


template<class T1, class T2>
class Person
{
public:
  T1 name;
  T2 height;
  Person(T1 name, T2 height);//构造函数声明
  void showInfo();//showInfo函数声明
};
//构造函数类外实现
template<class T1,class T2>
Person<T1,T2>::Person(T1 name, T2 height)
{
  this->height = height;
  this->name = name;
}
template<class t1, class t2>
void Person<t1,t2>::showInfo()
{
  cout << "姓名:" << name << " 身高:" << height << endl;
}
//成员函数类外实现
void test06()
{
  Person<string, int> p("叶落秋白",184);
  p.showInfo();
}


         虽然在加作用域的前提下加了类模板的参数列表,但是编译器并不认识那些虚拟类型,所以还需要在其上面紧跟着加上template关键字来声明虚拟类型。


类模板的分文件编写

问题所在:

由于类模板成员函数在调用时才创建,只包含头文件,再函数调用时会出现无法解析的命令

解决方案:


1、包含源文件(.cpp)不常用

2、常将头文件和源文件写进一个文件里,文件后缀名改为.hpp,约定的格式


示例:


//person.hpp文件内容
#pragma once
#include<iostream>
using namespace std;
template<class T1, class T2>
class Person
{
public:
  T1 name;
  T2 height;
  Person(T1 name, T2 height);
  void showInfo();
};
//构造函数类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 height)
{
  this->height = height;
  this->name = name;
}
template<class t1, class t2>
void Person<t1, t2>::showInfo()
{
  cout << "姓名:" << name << " 身高:" << height << endl;
}
//调用的文件内容
#include"person.hpp"
void test07()
{
  Person<string, int> p("叶落秋白", 184);
  p.showInfo();
}


       将头文件声明和源文件实现全部写在一个头文件里并将头文件名改为.hpp就能解决类模板的分文件编写出现的问题了。


类模板和友元

特点:


全局函数类内实现,直接在类内声明友元即可

全局函数类外实现,需要告诉编译器类以及友元函数的实现,写在最上方


代码讲解:


//全局函数设置在类外时需要做的工作
template<class T1, class T2>
class Student;
template<class T1, class T2>
void showInfo1(Student<T1, T2>p)
{
  cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
}
//类模板Student
template<class T1, class T2>
class Student
{
  //1、全局函数设置在类内
  friend void showInfo(Student p)
  {
  cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
  }
  //2、全局函数设置在类外
  friend void showInfo1<>(Student s);
private:
  T1 name;
  T2 age;
public:
  Student(T1 name, T2 age)
  {
  this->age = age;
  this->name = name;
  }
};
void test08()
{
  Student<string, int> s1("叶落秋白", 18);
  Student<string, int> s2("微凉秋意", 20);
  showInfo(s1);
  showInfo1(s2);
}


       首先我创建类模板Student将name 和 age 属性进行封装,我们知道通过friend可以将函数设置为友元函数,即可以访问类的私有属性。全局函数内部实现比较简单,直接在函数前面加上friend关键字即可。


全局函数的外部实现稍微复杂点:


       首先将函数的声明写在类上方并在前面加上friend关键字,然后在类外完成该友元函数的实现。要特别注意:类外实现的时候需要加上template关键字声明虚拟类型,那么此时声明的部分也要加上空列表才行,要不然声明和实现不对应;而且类模板函数只有在调用时才会创建,所以需要把具体实现写在类上方,这样编译器才会创建这个函数;那么当你把实现写在最上面时,参数列表需要指定类型,那么就需要再把类模板的声明写在该实现上方。只有完成这些工作之后,才可以实现友元函数的类外实现。


运行效果:



相关文章
|
4天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
24 5
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
24 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
16 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1