多态
多态的概念
多态是面向对象编程中的一个重要概念,它允许不同类型的对象对同一个消息作出不同的响应。具体来说,多态性是指在基类中定义的虚函数,在派生类中可以被重写,并且在运行时根据对象的实际类型来调用相应的函数。
多态性的实现依赖于继承和虚函数的机制。通过将函数声明为虚函数,可以使得在派生类中重写该函数,从而实现不同的行为。当通过基类指针或引用调用虚函数时,实际上会根据对象的实际类型来调用相应的函数,而不是根据指针或引用的类型。
多态性的优势在于它提高了代码的灵活性和可扩展性。通过使用多态性,可以编写通用的代码,能够处理不同类型的对象,而无需为每个具体类型编写特定的代码。这样可以简化代码的维护和扩展,并且使得代码更加可复用。
总结起来,多态性是面向对象编程中的一个重要概念,它允许不同类型的对象对同一个消息作出不同的响应。通过继承和虚函数的机制,可以实现多态性,提高代码的灵活性和可扩展性。
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
多态案例
#include <iostream>
using namespace std;
#include<string>
//木材
class Wood {
protected:
string name;
int weight;
public:
Wood(string n ,int w):name(n),weight(w){}
virtual void Show() {//虚函数
cout << "这是一块重" << weight << "kg的" << name << endl;
}
};
//凳子
class Stool :public Wood{
public:
Stool(string n , int w):Wood(n,w){}
void Show() {//虚函数
cout << "这是用一块重" << weight << "kg的" << name << "做的凳子" << endl;
}
};
//桌子
class Dest :public Wood {
public:
Dest(string n, int w) :Wood(n, w) {}
void Show() {//虚函数
cout << "这是用一块重" << weight << "kg的" << name << "做的桌子" << endl;
}
};
void test(Wood* p) {
p->Show();
}
int main() {
Wood w1("橡木", 5), w2("楠木", 6);
Stool s1("橡木", 5), s2("楠木", 6);
Dest d1("橡木", 5), d2("楠木", 6);
w1.Show();
w2.Show();
cout << "------------------------------" << endl;
s1.Show();
s2.Show();
cout << "------------------------------" << endl;
d1.Show();
d2.Show();
/*
这是一块重5kg的橡木
这是一块重6kg的楠木
------------------------------
这是用一块重5kg的橡木做的凳子
这是用一块重6kg的楠木做的凳子
------------------------------
这是用一块重5kg的橡木做的桌子
这是用一块重6kg的楠木做的桌子
*/
cout << "------------------------------" << endl;
Wood* p;//多肽的体现 同样的指针有不同的结果
p = &w1;
test(p);
cout << "------------------------------" << endl;
p = &s1;
test(p);
cout << "------------------------------" << endl;
p = &d1;
test(p);
/*这是一块重5kg的橡木
------------------------------
这是用一块重5kg的橡木做的凳子
------------------------------
这是用一块重5kg的橡木做的桌子*/
/*如果没加virtual 则全为“这是一块重5kg的橡木”*/
//引用也可以
Dest d1("楠木", 10);
Wood* W1;
W1 = &d1;
W1->Show();
return 0;
}
多态的实现靠的是虚函数表
虚函数表是链表
虚函数
虚函数是一种单界面多实现版本的实现方法,即函数名、返回类型、函数类型和个数顺序完全相同,但函数体内容可以完全不同。
基类中采用virtual说明一个虚函数后,派生类中定义相同原型函数时可不必加virtual说明
虚函数不允许说明成静态的成员函数
虚函数例子
下面是一个使用C++虚函数的例子:
#include <iostream>
class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape" << std::endl;
}
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle" << std::endl;
}
};
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
shape1->draw(); // 输出 "Drawing a circle"
shape2->draw(); // 输出 "Drawing a rectangle"
delete shape1;
delete shape2;
return 0;
}
在上面的例子中,Shape是一个基类,它有一个虚函数draw()。Circle和Rectangle是Shape的派生类,它们分别重写了draw()函数。
在main函数中,我们创建了两个Shape类型的指针shape1和shape2,并分别指向Circle和Rectangle的对象。当我们调用shape1和shape2的draw()函数时,实际上会根据对象的实际类型来调用相应的函数。这就是虚函数的多态性。
输出结果为:
Drawing a circle
Drawing a rectangle
可以看到,shape1调用draw()时输出了"Drawing a circle",而shape2调用draw()时输出了"Drawing a rectangle",这正是因为虚函数的多态性。
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
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();
system("pause");
return 0;
}
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
重载、重写、重定义
函数重载(overload)
函数重载是指在一个类中声明多个名称相同但参数列表不同的函数,这些的参数可能个数或顺序,类型不同,但是不能靠返回类型来判断。特征是:
(1)相同的范围(在同一个作用域中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无(注:函数重载与有无virtual修饰无关);
(5)返回值可以不同;
函数重写(也称为覆盖 override)
函数重写是指子类重新定义基类的虚函数。特征是:
(1)不在同一个作用域(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有 virtual 关键字,不能有 static 。
(5)返回值相同,否则报错;
(6)重写函数的访问修饰符可以不同;
重定义(也称隐藏)
(1)不在同一个作用域(分别位于派生类与基类) ;
(2)函数名字相同;
(3)返回值可以不同;
(4)参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆);
(5)参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆);