为什么C++需要虚函数?
技术背景
在C++中,继承是一个重要的特性,它允许一个类(派生类)继承另一个类(基类)的属性和方法。然而,当通过基类指针或引用调用派生类对象的方法时,可能会出现一些问题。为了解决这些问题,C++引入了虚函数的概念。虚函数是实现运行时多态的关键,它允许在运行时根据对象的实际类型来决定调用哪个函数。
实现步骤
1. 定义基类和派生类
首先,定义一个基类,并在其中声明虚函数。然后,定义派生类,并重写基类的虚函数。
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
virtual void eat() {
cout << "I'm eating generic food." << endl;
}
};
// 派生类
class Cat : public Animal {
public:
void eat() override {
cout << "I'm eating a rat." << endl;
}
};
2. 通过基类指针调用虚函数
在主函数中,创建基类和派生类的对象,并使用基类指针指向派生类对象,然后调用虚函数。
int main() {
Animal* animal = new Animal();
Cat* cat = new Cat();
Animal* animalCat = cat;
animal->eat(); // 输出: "I'm eating generic food."
cat->eat(); // 输出: "I'm eating a rat."
animalCat->eat(); // 输出: "I'm eating a rat."
delete animal;
delete cat;
return 0;
}
核心代码
虚函数实现运行时多态
#include <iostream>
using namespace std;
class Animal {
public:
virtual void MakeTypicalNoise() = 0; // 纯虚函数,使Animal成为抽象类
virtual ~Animal() {};
};
class Cat : public Animal {
public:
void MakeTypicalNoise() override {
cout << "Meow!" << endl;
}
};
class Dog : public Animal {
public:
void MakeTypicalNoise() override {
cout << "Woof!" << endl;
}
};
class Doberman : public Dog {
public:
void MakeTypicalNoise() override {
cout << "Woo, woo, woow! ... ";
Dog::MakeTypicalNoise();
}
};
int main() {
Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };
const int cnAnimals = sizeof(apObject) / sizeof(Animal*);
for (int i = 0; i < cnAnimals; i++) {
apObject[i]->MakeTypicalNoise();
}
for (int i = 0; i < cnAnimals; i++) {
delete apObject[i];
}
return 0;
}
虚析构函数的使用
#include <iostream>
using namespace std;
class Animal {
public:
virtual ~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat : public Animal {
public:
~Cat() {
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal* a = new Cat();
delete a;
return 0;
}
最佳实践
设计抽象基类
将基类设计为抽象基类,包含纯虚函数,这样可以强制派生类实现这些函数,确保派生类具有必要的功能。
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual ~Shape() {}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14 * radius * radius;
}
};
使用override关键字
在派生类中重写虚函数时,使用override关键字,这样可以确保重写的函数与基类的虚函数签名一致,避免因拼写错误等原因导致的问题。
合理使用虚析构函数
当基类指针指向派生类对象时,为了确保在删除基类指针时能够正确调用派生类的析构函数,避免内存泄漏,应将基类的析构函数声明为虚析构函数。
常见问题
虚函数的效率问题
虚函数的调用涉及到运行时的动态绑定,因此比普通函数调用稍微慢一些。但是,在现代编译器和硬件的支持下,这种性能损失通常是可以接受的。
忘记使用virtual关键字
如果在基类中忘记将函数声明为虚函数,那么通过基类指针调用该函数时,将不会发生动态绑定,而是调用基类的函数。
纯虚函数的使用
纯虚函数必须在派生类中实现,否则派生类将成为抽象类,不能实例化。
虚函数表的开销
每个包含虚函数的类都有一个虚函数表,每个对象都有一个指向虚函数表的指针,这会增加一定的内存开销。