多态
1、多态的基本概念
多态的分类:
静态多态:函数重载和运算符重载 属于静态多态,复用函数名
动态多态:派生类和虚函数 实现运行时多态
静态多态和动态多态的区别:
静态多态 的函数地址 早绑定 - 编译阶段确定函数地址
动态多态 的函数地址 晚绑定 - 运行阶段确定函数地址
class Animal1
{
public:
//虚函数 -> 地址晚绑定
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat2 : public Animal1
{
public:
virtual void speak()//子类的 virtual 可写可不写
{
cout << "小猫在说话" << endl;
}
};
//执行说话函数
void DoSpeak(Animal1 &animal) //Animal1 &animal = c1
{
animal.speak();
}
//地址早绑定,在编译阶段确定函数地址
//如果想让“猫说话”,那么这个函数地址就不能早绑定,需要在运行阶段绑定,即晚绑定。
void test45()
{
Cat2 c1;
DoSpeak(c1);
}
void main45()
{
test45();
system("pause");
}
总结:
动态多态满足条件:
- 有继承关系
- 子类重写父类的虚函数
重写:函数的返回值类型、函数名、形参列表 完全相同
动态多态的使用:父类的指针或引用 执行子类的对象
///多态的底层原理:
class Animal3
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat3 : public Animal3
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//执行说话函数
void DoSpeak3(Animal3& animal)
{
animal.speak();
}
void test46()
{
cout << "size of Animal3 = " << sizeof(Animal3) << endl; // = 1 // void speak(){} 不加virtual时
cout << "size of Animal3 = " << sizeof(Animal3) << endl; // = 4 // virtual void speak(){} 加virtual时
/*
原因剖析:
Animal类内部结构:
Animal类内部有一个 vfptr(虚函数(表)指针),指向vftable(虚函数表);
表内部会记录一个虚函数的地址 &Animal::sperk
直接继承时:
Cat类内部结构:
Cat类内部有一个 vfptr(虚函数(表)指针),指向vftable(虚函数表);
表内部会记录一个虚函数的地址 &Animal::sperk
重写:
重写后,子类继承父类的虚函数,子类中的虚函数表会替换成子类的虚函数地址 &Cat::sperk
Cat类内部结构:
Cat类内部有一个 vfptr(虚函数(表)指针),指向vftable(虚函数表);
表内部会记录一个虚函数的地址 &Cat::sperk
*/
}
int main46()
{
test46();
system("pause");
return 0;
}
2、纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要是调用子类重写的内容,因此可以将虚函数改为 纯虚函数
当类中有了纯虚函数,这个类也称为 抽象类
//纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
抽象类的特点:
- 无法实例化对象;
- 子类必须重写抽象类中的纯虚函数,否则也属于 抽象类;
class Base
{
public :
virtual void func() = 0;
};
class Son : public Base {};
class Son0 : public Base
{
public :
void func()
{
cout << "Son0 的 func函数的调用" << endl;
}
};
void test50()
{
//1.无法实例化对象
//Base b1; //无法实例化抽象类
//new Base;
//2.子类必须重写抽象类中的纯虚函数,否则也属于 抽象类;
//Son s1;
Son0 s2;
Base* base = new Son0;
base->func();
}
int main50()
{
test50();
system("pause");
return 0;
}
3、虚析构和纯虚析构
多态使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数,会造成内存泄漏
解决方式:将父类中的析构函数改成 虚析构或者纯虚析构
虚析构和纯虚析构的共性:
- 父类指针可以释放子类对象;
- 都需要有具体的函数实现;
虚析构和纯虚析构的区别:
- 如果是纯虚析构,则该类属于抽象类,无法实例化对象
语法:
虚析构函数:
virtual ~类名() {};
纯虚析构函数:
virtual ~类名() = 0;
类名::~类名() {};
class Animal4
{
public :
Animal4()
{
cout << "Animal的构造函数调用" << endl;
}
//虚构函数 需要声明
//virtual ~Animal4()//利用虚析构解决父类指针释放子类对象时不干净的问题
//{
// cout << "Animal的析构函数调用" << endl;
//}
//纯虚析构函数 需要声明 + 实现
virtual ~Animal4() = 0;//必须有具体的函数实现
virtual void Speak() = 0;
};
Animal4::~Animal4()
{
cout << "Animal的纯析构函数调用" << endl;
}
class Cat4 : public Animal4
{
public :
string * m_Name;
Cat4(string name)
{
cout << "Cat的构造函数调用" << endl;
m_Name = new string(name);
}
~Cat4()
{
if (m_Name != NULL)
{
cout << "Cat的析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话" << endl;
}
};
void test52()
{
Animal4* animal = new Cat4("汤姆");
animal->Speak();
delete animal;
//父类函数在析构时不会调用子类的析构函数,导致如果子类有堆区属性,会造成内存泄漏
//解决办法:将父类的析构改成虚析构函数
}
int main52()
{
test52();
system("pause");
return 0;
}
总结:
- 虚析构和纯虚析构函数就是用来解决 无法通过父类指针释放子类对象的问题;
- 如果子类中没有堆区数据,可以不写虚析构和纯虚析构函数;
- 拥有纯虚析构函数的类也属于抽象类;
(哔哩哔 哩黑马程序员 C++教程 学习笔记,如有侵权请联系删除)