多态(Polymorphism) 是面向对象编程(OOP)的三大特性之一(另外两个是 封装 和 继承)。多态的意思是“多种形态”,它允许不同的对象对同一消息作出不同的响应。简单来说,多态是指通过统一的接口调用不同的实现。
1. 多态的核心思想
多态的核心思想是:
-
同一操作作用于不同的对象,可以有不同的解释,产生不同的结果。
-
例如,动物都会“叫”,但不同的动物(如猫、狗)的叫声是不同的。通过多态,我们可以用统一的“叫”接口调用不同动物的叫声。
2. 多态的分类
多态可以分为两类:
-
编译时多态(静态多态):
-
在编译时确定具体的函数调用。
-
例如:函数重载(Overloading)和运算符重载。
-
-
运行时多态(动态多态):
-
在运行时确定具体的函数调用。
-
例如:通过基类的指针或引用调用派生类的重写函数(虚函数)。
-
3. 多态的实现方式
在 C++ 中,多态主要通过以下方式实现:
(1)函数重载(Overloading)
函数重载是指在同一个作用域内定义多个同名函数,但这些函数的参数列表不同(参数类型、参数个数或参数顺序)。
#include <iostream>
using namespace std;
void print(int i)
{
cout << "Integer: " << i << endl;
}
void print(double d)
{
cout << "Double: " << d << endl;
}
void print(string s)
{
cout << "String: " << s << endl;
}
int main()
{
print(10); // 调用 void print(int)
print(3.14); // 调用 void print(double)
print("Hello"); // 调用 void print(string)
return 0;
}
输出:
Integer: 10
Double: 3.14
String: Hello
特点:
-
编译时确定调用哪个函数。
-
属于静态多态。
(2)运算符重载
运算符重载是指为自定义类型定义运算符的行为。
#include <iostream>
using namespace std;
class Complex
{
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other)
{
return Complex(real + other.real, imag + other.imag);
}
};
int main()
{
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2; // 调用重载的 + 运算符
cout << "c3 = (" << c3.real << ", " << c3.imag << ")" << endl;
return 0;
}
特点:
-
编译时确定调用哪个运算符函数。
-
属于静态多态。
(3)虚函数(Virtual Function)
虚函数是实现运行时多态的关键。通过基类的指针或引用调用派生类的重写函数。
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()//虚函数
{
cout << "Animal speaks " << endl;
}
};
class Dog :public Animal//继承
{
public:
void speak()override//重写虚函数
{
cout << "Dogs barks" << endl;
}
};
class Cat :public Animal//继承
{
public:
void speak()override
{
cout << "Cat meows " << endl;
}
};
int main()
{
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->speak();
animal2->speak();
delete animal1;
delete animal2;
return 0;
}
输出:
Dogs barks
Cat meows
虚函数(Virtual Function) 是 C++ 中实现 运行时多态(Runtime Polymorphism) 的关键机制。它允许通过基类的指针或引用调用派生类的重写函数,从而实现动态绑定(Dynamic Binding)。下面我会详细讲解虚函数的概念、用法、原理以及注意事项。
1. 虚函数的基本概念
(1)什么是虚函数?
虚函数是在基类中使用 virtual
关键字声明的成员函数。派生类可以重写(Override)虚函数,从而提供自己的实现。
(2)虚函数的作用
-
实现 运行时多态:在程序运行时,根据实际对象的类型调用相应的函数。
-
提供 统一的接口:通过基类的指针或引用调用派生类的函数。
2. 虚函数的语法
(1)声明虚函数
在基类中使用 virtual
关键字声明虚函数:
class Base
{
public:
virtual void show()
{
cout << "Base class show()" << endl;
}
};
(2)重写虚函数
在派生类中重写虚函数时,可以使用 override
关键字(C++11 引入)来显式表明重写:
class Derived : public Base
{
public:
void show() override
{ // 重写基类的虚函数
cout << "Derived class show()" << endl;
}
};
3. 虚函数的使用示例
#include<iostream>
using namespace std;
class Base//基类
{
public:
virtual void show()
{
cout << "Base class show()" << endl;
}
};
class Derived :public Base//继承
{
public:
void show() override
{
cout << "Derived class show()" << endl;
}
};
int main()
{
Base* baseptr;
Derived derivedobj;
baseptr = &derivedobj;//基类指针指向派生对象
baseptr->show();
}
输出:
Derived class show()
代码说明:
-
basePtr
是基类指针,但它指向派生类对象derivedObj
。 -
通过基类指针调用
show()
时,实际调用的是派生类的show()
函数。
4. 虚函数的原理
(1)虚表(Virtual Table, VTable)
-
每个包含虚函数的类都有一个虚表。
-
虚表是一个函数指针数组,存储了该类所有虚函数的地址。
(2)虚表指针(Virtual Table Pointer, VPtr)
-
每个对象在内存中都有一个隐藏的虚表指针,指向其类的虚表。
-
当调用虚函数时,程序通过虚表指针找到虚表,再从虚表中找到对应的函数地址并调用。
(3)动态绑定的实现
-
在编译时,编译器无法确定基类指针指向的具体对象类型。
-
在运行时,程序通过虚表指针和虚表确定实际调用的函数。
5. 纯虚函数与抽象类
(1)纯虚函数
纯虚函数是在基类中声明但没有实现的虚函数,语法如下:
virtual void func() = 0;
(2)抽象类
-
包含纯虚函数的类称为抽象类。
-
抽象类不能实例化,只能作为基类。
-
派生类必须实现纯虚函数,否则派生类也会成为抽象类。
示例:
#include<iostream>
using namespace std;
class Shape
{
public:
virtual void draw() = 0;//纯虚函数
};
class Circle :public Shape
{
public:
void draw()override
{
cout << "Drawing a circle" << endl;
}
};
int main()
{
//Shape shape;//错误,不能实例化抽象类
Circle circle;
circle.draw();
return 0;
}
输出:
Drawing a circle
6. 虚函数的注意事项
(1)构造函数不能是虚函数
-
构造函数在对象创建时调用,此时虚表指针尚未初始化,因此不能使用虚函数机制。
(2)析构函数应该是虚函数
-
如果基类的析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源泄漏。
#include<iostream>
using namespace std;
class Base
{
public:
virtual ~Base()//虚析构函数
{
cout << "Base destructor" << endl;
}
};
class Derived :public Base
{
public:
~Derived()
{
cout << "Derived destructor" << endl;
}
};
int main()
{
Base* baseptr = new Derived();
delete baseptr;//调用析构函数
return 0;
}
输出:
Derived destructor
Base destructor
(3)虚函数的性能开销
-
虚函数需要通过虚表查找函数地址,因此比普通函数调用稍慢。
-
在性能敏感的代码中,应避免过度使用虚函数。
7. 总结
-
虚函数是实现运行时多态的关键机制。
-
通过虚表和虚表指针,程序可以在运行时确定实际调用的函数。
-
纯虚函数和抽象类用于定义接口,强制派生类实现特定功能。
-
析构函数应该是虚函数,以确保正确释放资源。
回到多态:
4. 多态的优点
-
代码复用:
通过基类接口调用派生类的方法,减少重复代码。 -
扩展性:
可以轻松添加新的派生类,而不需要修改基类代码。 -
灵活性:
程序可以在运行时根据实际对象类型调用相应的方法。
5. 多态的应用场景
-
图形绘制:
-
基类
Shape
定义虚函数draw()
,派生类Circle
、Rectangle
实现各自的draw()
方法。
-
-
游戏开发:
-
基类
Enemy
定义虚函数attack()
,派生类Monster
、Boss
实现各自的attack()
方法。
-
-
插件系统:
-
通过基类接口调用不同插件的功能。
-
6. 总结
-
多态是面向对象编程的重要特性,允许通过统一的接口调用不同的实现。
-
多态分为 编译时多态(如函数重载、运算符重载)和 运行时多态(如虚函数)。
-
运行时多态通过虚函数实现,是 C++ 中最常用的多态方式。