一、定义
在不同继承关系的类对象中,去调用同一函数,产生了不同的行为,即为多态。
例如我们定义一个动物类,在其中定义一个cry()函数。由于动物分为很多种,所以我们在继承该类时还需要重新编写cry()函数,重新定义叫声;这种方式称为虚函数重写。
什么是虚函数?
多态性在C++中是通过虚函数实现的。
虚函数:就是父类允许被其子类重新定义的成员函数,而子类重新定义父类函数的做法,称为“覆盖”,或者称为“重写”。
子类重写父类中虚函数时,即使没有virtual声明,该重载函数也是虚函数。
虚函数:在类的成员函数前加virtual关键字。
什么是虚函数的重写?
虚函数的重写:派生类中有一个跟基类的完全相同的虚函数,我们就称子类的虚函数重写了基类的虚函数。“完全相同”是指:函数名、参数、返回值都相同。另外,虚函数的重写也叫做虚函数的覆盖。
class Animal //动物类
{
public:
virtual void cry()
{
cout << "动物在叫" << endl; //动物在叫
}
};
class Dog : public Animal //狗
{
public:
virtual void cry() //子类完成对父类虚函数的重写
{
cout << "汪汪" << endl;//小狗再叫
}
};
class Cat: public Animal //动物类
{
public:
virtual void cry() //子类完成对父类虚函数的重写
{
cout << "喵喵" << endl;//人在叫
}
};
在继承中要构成多态还需要两个条件:
a. 调用函数的对象必须是指针或者引用。
b. 被调用的函数必须是虚函数,且完成了虚函数的重写。
#include<iostream>
using namespace std;
class Animal //动物类
{
public:
virtual void cry()
{
cout << "动物在叫" << endl; //动物在叫
}
};
class Dog : public Animal //狗
{
public:
void cry() //子类完成对父类虚函数的重写
{
cout << "汪汪" << endl;//小狗再叫
}
};
class Cat: public Animal //动物类
{
public:
virtual void cry() //子类完成对父类虚函数的重写,且子函数中virtual可加可不加
{
cout << "喵喵" << endl;//人在叫
}
};
void DoCry(Animal & animal)
{
animal.cry();
}
void MakeCry(Animal * p)
{
p->cry();
}
int main()
{
Animal st;
Dog p;
DoCry(st);//子类对象切片过去
DoCry(p);//父类对象传地址
Animal bt;
Cat q;
MakeCry(&bt);
MakeCry(&q);
}
注意:
- 不规范的重写行为。在派生类中重写的成员函数可以不加virtual关键字,也是构成重写,因为继承后基类的虚函数被继承下来,在派生类中依旧保持虚函数的属性,我们只是重写了它。这是非常不规范的,在平时尽量不要这样使用。若子类中的函数有virtual修饰,而父类中没有,则会构成函数隐藏。
- 通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数,所以我们需要构构建虚函数。
- C++中虚函数的唯一用处就是构成多态。C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量
二、纯虚函数与抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容 ;因此可以将虚函数声明为纯虚函数,语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0
,表明此函数为纯虚函数。
最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
包含纯虚函数的类称为抽象类。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
#include<iostream>
using namespace std;
class Animal //动物类
{
public:
virtual void cry()=0;
};
class Dog : public Animal //狗
{
public:
void cry() //子类完成对父类虚函数的重写
{
cout << "汪汪" << endl;//小狗再叫
}
};
class Cat: public Animal //动物类
{
public:
virtual void cry() //子类完成对父类虚函数的重写,且子函数中virtual可加可不加
{
cout << "喵喵" << endl;//人在叫
}
};
void DoCry(Animal & animal)
{
animal.cry();
}
void MakeCry(Animal * p)
{
p->cry();
}
int main()
{
Dog p;
DoCry(p);//父类对象传地址
Cat q;
MakeCry(&q);
}
注意:
- 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
- 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。如下例所示:
//顶层函数不能被声明为纯虚函数
void fun() = 0; //compile error
class base{
public :
//普通成员函数不能被声明为纯虚函数
void display() = 0; //compile error
};
三、虚析构与纯虚析构
存在的目的:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码 。
#include<iostream>
#include<string>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
~Animal()
{
cout << "Animal析构函数调用!" << endl;
}
virtual void Speak() = 0;
};
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->Speak();
delete animal;
}
int main()
{
test01();
return 0;
}
由上述程序可以发现子类的析构函数并没有调用。 父类指针在析构的时候,不会调用子类的析构函数,导致子类如果有堆区属性,会出现内存的泄露情况。如何解决此种问题呢?
1、将父类指针换成子类指针
#include<iostream>
#include<string>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
~Animal()
{
cout << "Animal析构函数调用!" << endl;
}
virtual void Speak() = 0;
};
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Cat *animal = new Cat("Tom");//此处Animal换成Cat
animal->Speak();
delete animal;
}
int main()
{
test01();
return 0;
}
2.使用虚析构与纯虚构
#include<iostream>
#include<string>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//析构函数加上virtual关键字,变成虚析构函数
virtual ~Animal()
{
cout << "Animal虚析构函数调用!" << endl;
}
};
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->Speak();
delete animal;
}
int main()
{
test01();
return 0;
}
#include<iostream>
#include<string>
using namespace std;
class Animal {
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//析构函数加上virtual关键字,变成虚析构函数
//virtual ~Animal()
// {
// cout << "Animal虚析构函数调用!" << endl;
//}
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用!" << endl;
};
//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->Speak();
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//怎么解决?给基类增加一个虚析构函数
//虚析构函数就是用来解决通过父类指针释放子类对象
delete animal;
}
int main()
{
test01();
return 0;
}
注意:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类