目录
一、C++ 编程世界的困惑
二、虚函数:神秘的钥匙
(一)初窥虚函数
(二)虚函数的独特规则
(三)虚函数的底层秘密
三、多态:编程世界的变形术
(一)多态的概念
(二)多态的分类
(三)多态的构成条件
(四)多态的实际应用
1、图形绘制系统
2、游戏开发中的角色行为控制
四、常见问题与注意事项
(一)构造函数与虚函数
(二)析构函数与虚函数
(三)其他注意事项
五、总结与展望
在 C++ 的编程世界里,你是否遇到过这样的情况:当你定义了一个基类和它的派生类,然后通过基类指针或引用去调用一个在派生类中被重写的函数时,却发现调用的并不是派生类中的版本,而是基类中的版本,这就好比你以为自己在开一辆炫酷的跑车(派生类),结果却发现它的速度和性能跟一辆普通轿车(基类)没什么两样,这是不是让人感到困惑又沮丧呢?
让我们通过一个简单的例子来看看这种情况。假设我们正在开发一个图形绘制程序,定义了一个基类Shape,它有一个绘制函数draw:
class Shape {
public:
void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};
然后,我们定义了一个派生类Circle,它继承自Shape,并重写了draw函数,用来绘制一个圆:
class Circle : public Shape {
public:
void draw() {
std::cout << "Drawing a circle." << std::endl;
}
};
现在,我们在主函数中尝试这样做:
int main() {
Shape* shapePtr = new Circle();
shapePtr->draw();
delete shapePtr;
return 0;
}
你可能会期待输出是 “Drawing a circle.”,但实际输出却是 “Drawing a shape.”。这是为什么呢?明明我们创建的是Circle对象,并且Circle类也重写了draw函数,按照常理应该调用Circle类中的draw函数才对呀。这就是 C++ 中虚函数和多态要解决的问题。接下来,让我们一起深入探究虚函数和多态的奥秘,看看如何让程序按照我们期望的方式运行。
虚函数是 C++ 中实现多态的关键机制,它就像是一把神秘的钥匙,能够打开多态编程的大门。简单来说,虚函数是使用virtual关键字修饰的类成员函数。当一个函数被声明为虚函数后,它就具备了一种特殊的性质:在通过基类指针或引用调用该函数时,编译器会根据指针或引用所指向的实际对象类型来决定调用哪个类的函数版本,而不是根据指针或引用的静态类型。这就是动态绑定的核心原理,也是多态实现的基础。
虚函数的重写并不是随意的,它有着严格的规则。首先,派生类中的虚函数必须与基类中的虚函数具有相同的函数名、参数列表和返回值类型(除了协变情况,稍后会详细介绍)。这就好比一场接力比赛,接力棒的传递必须按照既定的规则进行,每个选手都要在相同的位置、以相同的方式交接接力棒,才能保证比赛的顺利进行。
在 C++ 11 中,为了增强代码的可读性和可维护性,引入了override关键字。当在派生类中重写虚函数时,可以使用override关键字显式地声明该函数是对基类虚函数的重写。这样一来,如果不小心写错了函数签名,编译器会立即报错,从而避免了潜在的错误。
此外,还有一些特殊情况需要注意。例如,协变是指基类与派生类虚函数返回值类型不同,但满足一定条件时也能构成重写。具体来说,当基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,就称为协变。这就像是两个运动员在不同的赛道上奔跑,但他们的目标是一致的,最终都能到达终点。
析构函数的重写也是一个特殊情况。在 C++ 中,如果基类的析构函数不是虚函数,并且基类指针指向派生类对象时被删除,编译器将只调用基类的析构函数,而忽略派生类中可能存在的额外资源清理操作,这可能会导致内存泄漏或其他资源管理问题。为了避免这种情况,通常将基类的析构函数声明为虚函数。这样,当通过基类指针删除派生类对象时,会先调用派生类的析构函数,再调用基类的析构函数,确保资源得到正确释放。这就好比在拆除一座建筑物时,必须按照正确的顺序拆除各个部分,才能保证整个拆除过程的安全和顺利。
虚函数的实现原理涉及到虚函数表(virtual function table,简称 vtable)和虚函数指针(virtual function pointer,简称 vptr)。当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,这是一个存储虚函数地址的数组。每个对象都有一个虚函数指针,它指向所属类的虚函数表。在运行时,通过虚函数指针找到虚函数表,再根据虚函数在表中的索引找到对应的函数地址,从而实现动态绑定。这就像是一个复杂的导航系统,虚函数指针就像是 GPS 定位器,虚函数表则像是地图,通过它们的协作,程序能够准确地找到并调用正确的函数。
让我们通过一个简单的例子来进一步理解虚函数的底层实现。假设我们有一个基类Base和一个派生类Derived,Base类中包含一个虚函数func:
class Base {
public:
virtual void func() {
std::cout << "Base::func" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Derived::func" << std::endl;
}
};
在这个例子中,Base类有一个虚函数func,编译器会为Base类生成一个虚函数表,其中存放着func函数的地址。当创建一个Base类对象时,该对象会包含一个虚函数指针,指向Base类的虚函数表。同样,当创建一个Derived类对象时,Derived类对象也有一个虚函数指针,指向Derived类的虚函数表。由于Derived类重写了func函数,Derived类虚函数表中func函数的地址是Derived::func的地址。当通过基类指针或引用调用func函数时,程序会根据指针或引用所指向的实际对象类