1-3 多态、模板
1 多态
多态分两类:
- 静态多态:函数重载和运算符重载,即复用函数名
- 动态多态:派生类和虚函数来实现运行时多态
区别:
- 静态多态在编译阶段确定函数地址
- 动态多态在运行阶段确定函数地址,根据传入的对象不同确定具体的执行函数
动态多态满足条件:
- 首先要有继承关系
- 子类要重写父类的虚函数,重写的时候函数名和形参列表以及返回类型都要相同
动态多态的使用:
父类的指针或引用指向子类的对象
class Animal
{
public:
//虚函数
virtual void speak(){
cout<<"动物在说话"<<endl;
}
};
class Cat:public Animal
{
public:
void speak(){
cout<<"猫在说话"<<endl;
}
};
void test01(Animal &animal){
animal.speak();
}
int main()
{
Cat cat;
test01(cat);//父类引用可以指向子类
return 0;
}
如果父类中不是虚函数,输出就是动物在说话
变成虚函数后,输出就是猫在说话
父类函数为普通函数时,编译器就会在编译时认定调用对象就是animal,调用的函数地址就确定为animal中的speak;而虚函数会在运行阶段依据传入的参数来确定调用对象,进而确定是执行哪一个派生类的函数代码
2 多态的本质
前面已经知道,非静态成员和类是分开存储的,但是当我们用sizeof查看定义了虚函数的类之后,可以发现虚函数也是和静态成员一样和类存储在一起,它们是属于类的。
当我们在定义一个虚函数之后,类内会维护一个虚函数指针vfptr,而这个虚函数指针指向一个虚函数表vftable,在这个虚函数表中类的虚函数地址。当子类继承父类时,拷贝一份虚函数指针和虚函数表,如果子类重写了父类虚函数,那么子类会将虚函数表中的虚函数地址覆盖为自己的。
而多态就是可以传入不同的子类对象,子类对象拥有的虚函数表指针不同,会覆盖掉父类的虚函数表指针;因此会出现父类指向子类虚函数表,调用子类的虚函数,实现地址动态绑定,要注意的是,父类和子类会各自维护自己的虚函数表和表指针
3 多态的应用
设定一个计算器类
class Calculator
{
public:
int m_Num1;
int m_Num2;
int getResult(string opt){
if(opt == "+"){
return m_Num1 + m_Num2;
}else if(opt == "-"){
return m_Num1 - m_Num2;
}else if (opt == "*"){
return m_Num2 * m_Num1;
}
}
};
void test01(){
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout<<c.getResult("+");
}
int main()
{
test01();
return 0;
}
如果想要添加新的操作,这个时候就只能修改源码,实际中最好减少修改,但是允许扩展,所以接下来用多态实现
class AbstractCalculator//抽象类基类
{
public:
int m_Num1;
int m_Num2;
virtual int getResult(){
}
};
class AddCalculator:public AbstractCalculator
{
public:
int getResult(){
return m_Num1 + m_Num2;
}
};
class SubCalculator:public AbstractCalculator
{
public:
int getResult(){
return m_Num1 - m_Num2;
}
};
void test02(){
//加法
AbstractCalculator *abc = new AddCalculator;//父类指针指向子类对象
abc->m_Num1 = 10;
abc->m_Num2 = 5;
cout<<abc->getResult()<<endl;
delete abc;
//减法
abc = new SubCalculator;
abc->m_Num2 = 5;
abc->m_Num1 = 10;
cout<<abc->getResult()<<endl;
delete abc;
}
int main()
{
test02();
return 0;
}
由此可以看出多态的好处:
- 我们只需要拓展的去写新功能,基类定义好要做什么,子类进行具体实现,利用多态实现
- 结构清楚,利于维护
4 纯虚函数和抽象类
在多态中,通常父类中的虚函数不进行具体实现,主要还是子类重写内容
因此完全可以写成纯虚函数
virtual 返回值类型 函数名 (参数列表)= 0;
当类中有纯虚函数时,我们称该类为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
5 虚析构和纯虚析构
使用多态时。如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数
解决方法:将父类中的析构函数改为虚析构或者纯虚析构,用以解决父类指针释放子类对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名()= 0;
但是纯虚析构必须要有实现,比如在类外进行类名::~类名(){}
的实现,且拥有纯虚析构的类也属于抽象类,无法实例化对象
总结:
- 子类中有堆区数据时必须实现虚析构或纯虚析构,没有时不用写;
- 抽象类不能实例化对象
6 模板
1 模板的概念
模板就是建立通用的模具,大大提高复用性,比如PPt中的模板,样式框架已经放好了,把内容放进去就行,不同的内容实现了不同的ppt
C++中泛型编程主要用模板,模板有两种:
- 函数模板
- 类模板
2 函数模板
建立一个通用函数,函数返回值类型和形参类型可以不具体指定,用一个虚拟的类型表示
语法:
template<typename T>
函数声明或定义
- template--声明创建模板
- typename--表明其后面的符号是一种数据类型,可用class替代
- T--通用数据类型,也可用任意大写字母替换
使用:
比如我们要交换两个数,但是由于数据类型有多个,我们需要写多个函数swapInt、swapDouble....于是我们用模板来减少重复代码
template<typename T>//声明一个模板
void mySwap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
//使用函数模板
//1自动类型推到
mySwap(a,b);
//2.显示指定类型
mySwap<int>(a, b);
注意事项:
- 自动类型推导,必须推导出一致的数据类型T才可以使用
- 模板只有确定了类型T才能使用
比如:
template<typename T>
void func(){
cout<<1;
}
//使用
func<int>();
3 普通函数和函数模板之间的区别:
- 普通函数调用时可以发生自动类型转换(隐式转换)
- 函数模板调用时如果利用自动类型推导,没有自动类型转换
- 但是用显示调用函数模板时,可以自动类型转换
4 普通函数和函数模板的调用规则
- 如果函数模板和普通函数都实现了,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板,如myPrint<>(a, b);
- 函数模板也可以重载
- 如果函数模板可以产生更优的匹配,优先调用函数模板
5 函数模板的局限性
在使用函数模板的时候,内部会有比较之类的操作,如果遇到类对象作为参数的时候,就会出问题,这个时候需要为类对象另外开辟一条路径,比如:
template<typename T>
bool myCompare<T &a,T &b);
templat<> bool myCompare(Person &p1, Person &p2)
6 类模板
建立一个通用的类,类中成员和数据类型可以不具体指定
template<typename T,...>
类名
使用实例:
template<class NameType, class AgeType>
class Person
{
public:
NameType m_Name;
AgeType m_Age;
Person(NameType name, AgeType age){
this->m_Name = name;
this->m_Age = age;
}
};
int main()
{
Person<string, int> p1("qqqq",28);
return 0;
}
7 类模板和函数模板的区别
- 类模板没有自动类型推导,要显示使用给出参数类型
- 类模板允许默认参数,如:
template<class NameType, class AgeType = int>
8 类模板中成员函数创建时机
普通类中成员函数一开始就可以创建,类模板中成员函数在模板被调用时才去创建,因为类模板的类型一开始无法确定
9 类模板与继承
类模板被继承时要遵循以下规则:
- 子类继承的是父模板时,子类在声明的时候要指定父类中的T
- 如果不指定,编译器无法为子类分配内存
- 如果需要灵活指定出父类中T类型,子类也需要变为类模板
比如:
template<class T>
class Base
{
T m;
};
//如果不指定int,会报错
class Son:public Base<int>
{
};
//如果想灵活指定父类中T的类型,子类也写成可变模板,用T2来指定T
template<class T1, class T2>
class Son2:public Base<T2>
{
};
10 成员函数的类外实现
模板头和模板参数列表都要写
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age){}
11 类模板分文件编写
类模板中成员函数的创建实在调用阶段,导致分文件编写时链接不到
解决:
- 方式1:直接包含.cpp源文件
- 方式2:将声明和实现写到同一个文件中,并改后缀为.hpp,hpp是约定名称,不强制