一)多态的基本概念
在 C++ 中,多态(Polymorphism)是面向对象编程的核心特性之一,它允许不同类的对象对同一消息(函数调用)作出不同的响应。简单来说,多态就是 "多种形态",同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
多态的实现方式
虚函数(Virtual Functions):
虚函数是实现多态的关键机制。在基类中,将希望在派生类中被重写(Override)的函数声明为虚函数。声明虚函数的语法是在函数声明前加上关键字 virtual。例如:
cpp
class Shape
{
public:
virtual void draw() = 0; // 纯虚函数,抽象类
};
class Circle : public Shape
{
public:
void draw() override
{
// 绘制圆形的具体代码
}
};
class Rectangle : public Shape
{
public:
void draw() override
{
// 绘制矩形的具体代码
}
};
在这个例子中,Shape 类中的 draw 函数被声明为虚函数。Circle 类和 Rectangle 类继承了 Shape 类,并对 draw 函数进行了重写。override 关键字是 C++11 引入的,用于显式地表明函数是在重写基类中的虚函数,这有助于提高代码的可读性和可维护性,并且如果函数签名不匹配基类中的虚函数,编译器会报错。
抽象类(Abstract Classes)和纯虚函数(Pure Virtual Functions) :
抽象类是至少包含一个纯虚函数的类。纯虚函数是在声明时被赋值为 0 的虚函数,如 virtual void draw() = 0;。抽象类不能被实例化,它的主要作用是为派生类提供一个通用的接口定义。派生类必须重写抽象类中的纯虚函数,否则派生类也会成为抽象类。例如,在上面的 Shape 类就是一个抽象类,它定义了 draw 这个通用的绘制接口,Circle 和 Rectangle 等派生类通过重写 draw 函数来具体实现这个接口。
动态绑定(Dynamic Binding):
动态绑定是多态实现的底层机制。当通过基类指针或引用调用虚函数时,程序会在运行时根据对象的实际类型来决定调用哪个函数。例如:
cpp
Shape* shapePtr;
Circle circle;
Rectangle rectangle;
shapePtr = &circle;
shapePtr->draw(); // 调用Circle::draw()
shapePtr = &rectangle;
shapePtr->draw(); // 调用Rectangle::draw()
在这里,shapePtr 是一个指向 Shape 类的指针,它可以指向 Shape 类的派生类对象。在运行时,根据 shapePtr 所指向的实际对象(circle 或 rectangle),动态地决定调用 Circle::draw() 还是 Rectangle::draw(),这种在运行时确定函数调用的方式就是动态绑定。
多态的优点和应用场景
优点:
代码可扩展性和可维护性高:可以方便地添加新的派生类,只要这些派生类遵循基类定义的虚函数接口,就可以在不修改原有代码的基础上融入系统。例如,在图形绘制程序中,如果要添加一个新的图形 Triangle,只需要创建一个 Triangle 类,继承 Shape 类并实现 draw 函数即可,原有的绘制代码(通过基类指针或引用调用 draw 函数)可以直接使用新的图形类型。
增强了代码的灵活性和复用性:通过多态,可以编写通用的代码来处理不同类型的对象。例如,一个图形排序函数可以接受一个 Shape 指针数组,然后根据不同图形的面积大小进行排序,这个函数可以适用于任何 Shape 类的派生类,而不需要为每个图形类型单独编写排序函数。
应用场景:
图形用户界面(GUI)开发:在 GUI 框架中,各种控件(如按钮、文本框等)可以被看作是从一个基类(如 Widget)派生而来的。通过多态,当处理用户事件(如点击事件)时,可以通过基类指针或引用调用相应的事件处理函数,不同类型的控件(派生类)可以有不同的事件处理行为。
游戏开发:游戏中的各种实体(角色、道具等)可以继承自一个基类(如 GameObject)。通过多态,游戏引擎可以统一地处理这些实体的更新(如位置更新、状态更新等)和渲染(如绘制实体)操作,不同类型的实体可以有自己独特的更新和渲染逻辑。
数据库访问层:在数据库应用程序中,可以定义一个基类(如 DatabaseAccessor)来封装数据库访问的基本操作(如查询、插入等)。不同类型的数据库(如 MySQL、Oracle 等)可以通过派生类来实现这个基类,并根据各自数据库的特点重写访问操作函数。这样,应用程序的其他部分可以通过基类指针或引用统一地调用数据库访问操作,而不需要关心具体的数据库类型。
二)多态的几个重要概念和注意事项
1)多态分为两类
·1.静态多态:函数重载和运算符重载属于静态多态,复用函数名。
·2.动态多态:派生类和虚函数在实现运行时的多态。
2)静态多态和动态多态区别
·1.静态多态得函数地址早绑定,编译阶段确定函数地址。
·2.动态多态得函数地址晚绑定,运动阶段确定函数地址。
·3.动态多态满足条件:(1).有继承关系;(2).子类重写父类的虚函数。
·4.动态多态使用:(1).父类的指针或者引用指向子类对象。
3)多态的原理:
·1. 虚函数指针vfptr:v-virtual、f-function、ptr-pointer---指向一个虚函数表vf table。
表内部记录一个虚函数地址,需要加作用域表示虚函数中的函数地址 例如:&animal::speak。
·2.子类重写父类虚函数,此时子类虚函数表为继承的父类虚函数表。
·3.当父类引用指向子类时,子类的虚函数表会从父类虚函数表替换成子类虚函数表,此时发生多态。例如:animal & a=cat;&cat::speak。
4)在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,
因此可以将虚函数改为纯虚函数。
5)纯虚函数语法:virtual 返回值类型 函数名 (参数列表)=0。
6)当类中有了纯虚函数,这个类也称为抽象类。
7)抽象类特点
·1.无法实例化对象。
·2.子类必须重写(父类)抽象类中的纯虚函数,否则也属于抽象类。
cpp
#include<iostream>
using namespace std;
#include<string>
class base
{
public:
//纯虚函数---只要有一个纯虚函数,这个类就是抽象类
virtual void func() = 0;
};
class son :public base
{
public:
virtual void func()
{
cout << "son中fun调用" << endl;
};
};
void test1()
{
//base s;//抽象类无法实例化对象
//new base //抽象类无法实例化对象
son s;//子类必须重写父类中的纯虚函数,否则无法实例化对象
//多态调用
base* base = new (son);//开辟一块内存空间,存放son类,有无括号都可以
base->func();
}
int main()
{
test1();
system("pause");
return 0;
}
//纯虚函数目的:将基类变成抽象类,不能直接实例化,而只能作为其它类的基类使用,定义了一个统一的接口,保证派生类都可以实现这个函数,确保了一致性和统一性。
三)多态总结
1)多态使用时,如果子类中有成员变量开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码?
·1.解决方式:将父类中的析构函数改为虚析构或纯虚析构。
2)虚析构和纯虚析构的共性
·1.可以解决父类指针释放子类对象。
·2.都需要有具体的函数实现。
3)虚析构和纯虚析构区别
·1.如果是纯虚析构,该类属于抽象类,无法实例化对象。
·2.纯虚析构,需要声明,也需要实现。
4)语法:
·1.虚构语法:virtual ~类名(){}。
·2.纯虚构语法:virtual ~类名(){}=0。
5)步骤:
·1.首先创建animal类和cat类,其中cat类继承animal类。
·2.在类中都写有void speak()函数。
·3.在cat类中构建有参构造函数,并开辟name内存。
·4.将animal类中析构函数更改为虚析构或纯虚析构,这样父类指针指向子类对象,就可以释放子类对象的析构函数,从而释放子类开辟的内存。
cpp
#include<iostream>
using namespace std;
#include<string>
class base
{
public:
base()
{
cout << "base的构造函数调用" << endl;
}
//virtual~base()//改成虚析构,就会调用子类析构函数。解决父类指针释放子类对象时不干净的问题
//{
// cout << "base的析构函数调用" << endl;
//}
virtual~base() = 0;//纯虚析构,需要声明,也需要实现。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
base::~base()
{
cout << "base的纯虚析构函数调用" << endl;
};
class cat:public base
{
public:
cat(string name)
{
cout << "cat的构造函数调用" << endl;
m_name = new string(name);
}
virtual void speak()
{
cout << *m_name<<"小猫在说话" << endl;
}
~cat()
{
if (m_name != NULL)
{
cout << "cat的析构函数调用" << endl;
delete m_name;
m_name = NULL;
}
}
string *m_name;
};
void test()
{
base *base =new cat("tom");
base->speak();
//父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄露
delete base;
}
int main()
{
test();
system("pause");
return 0;
}
总结:
·1.虚析构或纯虚析构就是用来解决通过父类指针释放子类对象;
·2.如果子类中没有堆区数据,可以不写虚析构或纯虚析构函数;
·3.拥有纯虚析构函数的类也属于抽象类。