多态
多态是C++面向对象三大特性之一 三大特性是:封装 、 继承、 多态
多态分为两类
静态多态:函数重载 和 运算符重载 属于静态多态 , 复用函数名
动态多态 : 派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
void dospeak() {}
};
void dospeak(Animal &animal) //Animal &animal = cat;父类的指针可以指向子类
{
animal.speak();//speak相当于多种形态,传什么是什么
}
void test01()
{
Cat cat;
dospeak(cat);
}
在 dospeak函数中,animal.speak()
调用了 speak
方法。但是由于 speak
方法在 Animal
类中定义为非虚函数,因此在编译时就确定了调用的是 Animal
类的 speak
方法。这就是所谓的 静态绑定(或称早期绑定) 。
如果想执行让cat说话,那么这个函数地址就不能提前绑定,需要在运行阶段绑定,即地址晚绑定。
class Animal
{
public:
//虚函数 地址晚绑定
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
//重写概念:函数返回值类型 函数名 参数列表 完全相同 子类的virtual可以不写
void speak()
{
cout << "小猫在说话" << endl;
}
};
void dospeak(Animal &animal) //Animal &animal = cat;父类的指针可以指向子类
{
animal.speak();//speak相当于多种形态,传什么是什么
}
void test01()
{
Cat cat;
dospeak(cat);
}
动态多态的满足条件:1、有继承关系 2、子类要重写父类的虚函数
使用: 父类的指针或者引用 指向子类的对象
现在,我们来剖析动态多态的原理。首先,父类中创建了虚函数,其内部结构包含一个虚函数指针(vfptr),该指针指向虚函数表(vftable)。虚函数表中记录着虚函数的地址,例如 &Animal::speak()。
接下来,创建一个 Cat 类。如果 Cat 没有重写父类的虚函数,那么子类的虚函数表仍然指向父类的虚函数地址,即 &Animal::speak()。当 Cat 重写了父类的虚函数时,子类的虚函数表会更新,指向子类的虚函数地址,即 &Cat::speak()。此时,父类的指针或引用指向子类对象,就会发生多态。下面附上老师上课时的演示,并且我们可以通过开发者人员 查看对象模型模型 ,也可以自己了解多态的底层含义。
纯虚函数和抽象类
在多态中通常父类中的虚函数 的实现是毫无意义的,主要都是调用子类重写内容。
因此可以将虚函数改为纯虚函数。
纯虚函数语法: virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数 , 这个类也称为抽象类 强制子类必须重写。
抽象类的特点: 无法实例化对象 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
class Base
{
public:
//当类中有了纯虚函数 , 这个类也称为抽象类 强制子类必须重写
virtual void func() = 0; //要在虚函数 的基础上
};
class Son :public Base
{
public:
void func()
{
cout << "func函数调用" << endl;
}
};
void test01()
{
//Base b;栈区
//new Base;//堆区 抽象类无法实例化对象
//Son s1;//子类无重写纯虚函数 也无法实例化对象
Son s1;//子类必须要重写纯虚函数
}
在test01()中前面三行代码,第一行代码和第二行代码无法运行成功是因为 父类中含有纯虚函数,所以无法实例化对象,不管是在堆区还是栈区。第三行代码是在子类中,没有重写父类的纯虚函数,所以系统也会认为其是 抽象类,无法实例化对象。只有在子类中重写父类的虚函数,才可以实例化。
void test01()
{
/*Base b;栈区
new Base;*///堆区 抽象类无法实例化对象
//Son s1;//子类无重写纯虚函数 也无法实例化对象
Son s1;//子类必须要重写纯虚函数
Base* base = new Son;
base->func();
delete base;
base = new Son2;//new 哪个对象就调用哪个对象的函数
base->func();
delete base;
}
虚析构和纯虚析构
如果一个类含有纯虚析构,该类属于抽象类,无法实例化对象。
虚析构语法 virtual ~类名(){}
纯虚析构语法 virtual ~类名()=0;//需要类外写函数体 Animal::~Animal(){}
class Animal
{
public:
Animal()
{
cout << "animal的构造函数调用" << endl;
}
/*~Animal()
{
cout << "animal的析构函数调用" << endl;
}
virtual void speak() = 0;*/
//利用虚析构可以解决 父类指针释放子类对象时不干净的问题
//virtual ~Animal()//
//{
// cout << "animal的析构函数调用" << endl;
//}
virtual ~Animal() = 0;//纯虚析构需要有声明也需要实现
//有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
virtual void speak() = 0;
};
Animal:: ~Animal()
{
cout << "animal的纯虚析构函数调用" << endl;
};
class Cat :public Animal
{
public:
Cat(string name)
{
cout << "cat的构造函数调用" << endl;
m_name = new string(name);//创建在堆区 ,堆区要在析构释放
}
~Cat() //会发现cat的析构函数没有调用 需要在父类的析构前面加上virtual
{
if (m_name != NULL)
{
cout << "cat的析构函数调用" << endl;
delete m_name;
m_name = NULL;
}
}
virtual void speak()
{
cout <<*m_name<< "小猫在说话" << endl;
}
string *m_name;
};
void test01()
{
Animal* animal = new Cat("TOM");
animal->speak();
// 父类的指针在析构的时候不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄露
delete animal;
}
这里为什么要用到虚析构和纯虚析构呢,是因为,在子类中存在堆区数据,需要通过析构函数来释放,但是如果没有使用到虚析构或者纯虚析构,我们在运行的时候发现,不会调用到子类的析构函数,那也就是说子类析构函数中的函数体没有执行,导致堆区内存无法释放,造成数据泄露。
总结:虚析构或纯虚析构就是用来解决通过父类指针释放子类对象。
如果子类中没有堆区数据,可以不写为虚析构或者纯虚析构。
拥有纯虚析构函数的类也属于抽象类。