一、虚函数基础认识
大家在学 C++ 的时候,虚函数可是个挺重要的概念。简单来说,虚函数就是在基类里用 virtual 关键字声明的函数。为啥要有虚函数呢?这主要是为了实现多态。多态是啥?就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
咱来看个简单的例子:
// C++ 技术栈
#include <iostream>
// 基类 Animal
class Animal {
public:
// 虚函数 speak
virtual void speak() {
std::cout << "Animal speaks" << std::endl;
}
};
// 派生类 Dog 继承自 Animal
class Dog : public Animal {
public:
// 重写基类的虚函数 speak
void speak() override {
std::cout << "Dog barks" << std::endl;
}
};
// 派生类 Cat 继承自 Animal
class Cat : public Animal {
public:
// 重写基类的虚函数 speak
void speak() override {
std::cout << "Cat meows" << std::endl;
}
};
int main() {
// 创建基类指针
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
// 调用虚函数
animal1->speak();
animal2->speak();
// 释放内存
delete animal1;
delete animal2;
return 0;
}
在这个例子里,Animal 是基类,Dog 和 Cat 是派生类。speak 函数在基类里被声明为虚函数,在派生类里被重写。当我们用基类指针指向派生类对象时,调用 speak 函数会根据实际对象的类型来执行相应的函数,这就是多态的体现。
二、虚函数实现机制
虚函数是怎么实现的呢?其实主要靠两个东西:虚函数表(vtable)和虚表指针(vptr)。
每个包含虚函数的类都会有一个虚函数表,这个表就像是一个函数地址的列表,里面存着该类所有虚函数的地址。而每个该类的对象都会有一个虚表指针,这个指针指向该类的虚函数表。
当我们通过基类指针调用虚函数时,程序会先通过对象的虚表指针找到虚函数表,然后从虚函数表里找到对应的虚函数地址,最后调用该函数。
还是用上面的例子来说,Dog 和 Cat 对象都有自己的虚表指针,分别指向 Dog 和 Cat 类的虚函数表。当 animal1->speak() 被调用时,程序会通过 animal1 指向的 Dog 对象的虚表指针找到 Dog 类的虚函数表,然后从表中找到 Dog::speak 函数的地址并调用它。
三、性能优化
虚函数虽然很有用,但也有一些性能开销。因为每次调用虚函数都要通过虚表指针去找虚函数表,再从表中找函数地址,这就比普通函数调用多了一些步骤。
3.1 减少不必要的虚函数
如果一个函数不需要实现多态,就不要把它声明为虚函数。比如下面这个例子:
// C++ 技术栈
#include <iostream>
class Shape {
public:
// 非虚函数,不需要多态
void drawBorder() {
std::cout << "Drawing border" << std::endl;
}
// 虚函数,需要多态
virtual void drawShape() {
std::cout << "Drawing shape" << std::endl;
}
};
class Circle : public Shape {
public:
// 重写虚函数
void drawShape() override {
std::cout << "Drawing circle" << std::endl;
}
};
int main() {
Shape* shape = new Circle();
// 调用非虚函数
shape->drawBorder();
// 调用虚函数
shape->drawShape();
delete shape;
return 0;
}
在这个例子中,drawBorder 函数不需要多态,所以没有声明为虚函数,这样可以减少性能开销。
3.2 内联虚函数
如果虚函数的代码比较简单,并且调用频繁,可以考虑将其声明为内联函数。内联函数会在调用处直接展开代码,避免了函数调用的开销。不过要注意,内联函数只是一种建议,编译器可能不会真正内联。
// C++ 技术栈
#include <iostream>
class Base {
public:
// 内联虚函数
inline virtual void print() {
std::cout << "Base print" << std::endl;
}
};
class Derived : public Base {
public:
// 重写内联虚函数
inline void print() override {
std::cout << "Derived print" << std::endl;
}
};
int main() {
Base* base = new Derived();
base->print();
delete base;
return 0;
}
四、使用建议
4.1 正确使用 override 关键字
在派生类中重写虚函数时,使用 override 关键字可以让编译器帮我们检查是否正确重写了基类的虚函数。如果没有正确重写,编译器会报错。
// C++ 技术栈
#include <iostream>
class Base {
public:
virtual void func() {
std::cout << "Base func" << std::endl;
}
};
class Derived : public Base {
public:
// 使用 override 关键字重写虚函数
void func() override {
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base* base = new Derived();
base->func();
delete base;
return 0;
}
4.2 避免在构造函数和析构函数中调用虚函数
在构造函数和析构函数中调用虚函数可能会导致意外的结果。因为在构造函数执行时,派生类的部分还没有完全构造好;在析构函数执行时,派生类的部分已经被销毁了。
// C++ 技术栈
#include <iostream>
class Base {
public:
Base() {
// 不要在构造函数中调用虚函数
// this->print();
}
virtual void print() {
std::cout << "Base print" << std::endl;
}
~Base() {
// 不要在析构函数中调用虚函数
// this->print();
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived print" << std::endl;
}
};
int main() {
Base* base = new Derived();
delete base;
return 0;
}
五、应用场景
虚函数在很多场景下都很有用。比如在游戏开发中,不同的角色可能有不同的攻击方式,我们可以用虚函数来实现多态。
// C++ 技术栈
#include <iostream>
// 基类 Character
class Character {
public:
// 虚函数 attack
virtual void attack() {
std::cout << "Character attacks" << std::endl;
}
};
// 派生类 Warrior 继承自 Character
class Warrior : public Character {
public:
// 重写基类的虚函数 attack
void attack() override {
std::cout << "Warrior swings a sword" << std::endl;
}
};
// 派生类 Mage 继承自 Character
class Mage : public Character {
public:
// 重写基类的虚函数 attack
void attack() override {
std::cout << "Mage casts a spell" << std::endl;
}
};
int main() {
Character* character1 = new Warrior();
Character* character2 = new Mage();
character1->attack();
character2->attack();
delete character1;
delete character2;
return 0;
}
在这个游戏角色的例子中,不同的角色有不同的攻击方式,通过虚函数我们可以方便地实现多态,让代码更灵活。
六、技术优缺点
6.1 优点
- 实现多态:虚函数可以让我们通过基类指针或引用调用派生类的函数,实现多态,提高代码的灵活性和可扩展性。
- 代码复用:基类定义虚函数,派生类重写虚函数,避免了代码的重复编写。
6.2 缺点
- 性能开销:虚函数调用需要通过虚表指针和虚函数表,比普通函数调用多了一些步骤,会有一定的性能开销。
- 增加内存占用:每个包含虚函数的类都有一个虚函数表,每个该类的对象都有一个虚表指针,会增加内存占用。
七、注意事项
- 虚析构函数:如果基类有虚函数,建议将基类的析构函数声明为虚析构函数,这样在通过基类指针删除派生类对象时,能正确调用派生类的析构函数。
// C++ 技术栈
#include <iostream>
class Base {
public:
// 虚析构函数
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
virtual void print() {
std::cout << "Base print" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
void print() override {
std::cout << "Derived print" << std::endl;
}
};
int main() {
Base* base = new Derived();
base->print();
delete base;
return 0;
}
- 函数签名要一致:派生类重写虚函数时,函数签名(函数名、参数列表、返回类型)要和基类的虚函数一致,否则就不是重写,而是隐藏。
八、文章总结
虚函数是 C++ 中实现多态的重要手段,通过虚函数表和虚表指针来实现。虽然虚函数有一些性能开销,但在很多场景下能提高代码的灵活性和可扩展性。在使用虚函数时,我们要注意合理使用,减少不必要的虚函数,正确使用 override 关键字,避免在构造函数和析构函数中调用虚函数等。同时,要根据具体情况权衡虚函数的优缺点,选择合适的应用场景。
评论