虚函数与多态
定义与作用
虚函数是实现运行时多态(动态绑定)的机制。当通过基类指针或引用调用虚函数时,实际执行的是指针所指向的对象的真实类型的版本:
class Animal {
public:
virtual void speak() const { std::cout << "动物叫声" << std::endl; }
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "汪汪" << std::endl; }
};
Animal* pet = new Dog();
pet->speak(); // 输出 "汪汪"——动态绑定,而非 "动物叫声"
C++11 引入了两个关键的虚函数控制关键字:override(明确覆盖意图,编译器验证)和 final(阻止进一步覆盖或继承)。
核心原理
静态绑定 vs 动态绑定
override 与 final 的作用
完整示例
示例一:图形绘制系统——虚函数多态
场景说明:设计一个图形绘制系统,展示虚函数多态的实际应用。
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0; // 纯虚函数:每个形状必须实现
virtual double perimeter() const = 0;
virtual void draw() const {
std::cout << "绘制形状(面积: " << area()
<< ", 周长: " << perimeter() << ")" << std::endl;
}
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override {
return 3.14159 * radius_ * radius_;
}
double perimeter() const override {
return 2 * 3.14159 * radius_;
}
void draw() const override {
std::cout << "○ 圆形(半径: " << radius_ << ")" << std::endl;
}
private:
double radius_;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
double area() const override {
return width_ * height_;
}
double perimeter() const override {
return 2 * (width_ + height_);
}
void draw() const final { // final:子类不能再覆盖 draw
std::cout << "□ 矩形(" << width_ << "×" << height_ << ")" << std::endl;
}
private:
double width_, height_;
};
class Triangle : public Shape {
public:
Triangle(double a, double b, double c) : a_(a), b_(b), c_(c) {}
double area() const override {
double s = (a_ + b_ + c_) / 2;
return std::sqrt(s * (s - a_) * (s - b_) * (s - c_));
}
double perimeter() const override {
return a_ + b_ + c_;
}
void draw() const override {
std::cout << "△ 三角形(边长: " << a_ << ", " << b_ << ", " << c_ << ")" << std::endl;
}
private:
double a_, b_, c_;
};
int main() {
std::cout << "===== 图形绘制系统 =====" << std::endl;
// 基类指针容器——多态的关键
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(std::make_unique<Triangle>(3.0, 4.0, 5.0));
// 统一接口操作不同形状
for (const auto& shape : shapes) {
shape->draw();
}
std::cout << "\n===== 面积与周长统计 =====" << std::endl;
double total_area = 0.0;
for (const auto& shape : shapes) {
std::cout << "面积: " << shape->area()
<< " | 周长: " << shape->perimeter() << std::endl;
total_area += shape->area();
}
std::cout << "总面积: " << total_area << std::endl;
return 0;
}
预期输出:
===== 图形绘制系统 =====
○ 圆形(半径: 5)
□ 矩形(4×6)
△ 三角形(边长: 3, 4, 5)
===== 面积与周长统计 =====
面积: 78.5398 | 周长: 31.4159
面积: 24 | 周长: 20
面积: 6 | 周长: 12
总面积: 108.54
逐段分析:
Shape是抽象基类,定义纯虚函数area()和perimeter()——子类必须实现draw()是非纯虚函数,提供默认实现——子类可选择覆盖或沿用Rectangle::draw()标记为final——任何继承Rectangle的类不能再覆盖draw()std::vector<unique_ptr<Shape>>存储不同类型对象的基类指针——实现多态遍历- 每个
shape->draw()调用根据实际对象类型分派到对应的虚函数——运行时多态
示例二:override 的编译期保护
场景说明:展示 override 如何捕获常见的虚函数签名错误。
#include <iostream>
class Base {
public:
virtual void process(int value) const {
std::cout << "Base::process(int) = " << value << std::endl;
}
virtual void calculate(double x) {
std::cout << "Base::calculate(double) = " << x << std::endl;
}
};
// 错误示例(假设没有 override 保护)
class WrongDerived : public Base {
public:
// 错误 1:参数类型不匹配(int → unsigned int)→ 没有覆盖,而是新增重载
void process(unsigned int value) const { // 没有 override
std::cout << "WrongDerived::process(unsigned) " << value << std::endl;
}
// 错误 2:忘记 const → 没有覆盖
void calculate(double x) { // 没有 override
std::cout << "WrongDerived::calculate " << x << std::endl;
}
};
// 正确示例(使用 override 保护)
class CorrectDerived : public Base {
public:
void process(int value) const override { // 编译器验证:签名匹配 Base
std::cout << "CorrectDerived::process(int) = " << value << std::endl;
}
void calculate(double x) override {
std::cout << "CorrectDerived::calculate(double) = " << x << std::endl;
}
};
int main() {
std::cout << "===== 无 override 保护(隐藏而非覆盖) =====" << std::endl;
WrongDerived wrong;
wrong.process(10); // 调用 WrongDerived::process(unsigned)
wrong.process(10u); // 调用 WrongDerived::process(unsigned)
Base* base_ptr = &wrong;
base_ptr->process(10); // 调用 Base::process(int)!——不是覆盖,是隐藏
std::cout << "\n===== 有 override 保护(正确覆盖) =====" << std::endl;
CorrectDerived correct;
Base* correct_ptr = &correct;
correct_ptr->process(10); // 调用 CorrectDerived::process(int)
correct_ptr->calculate(3.14); // 调用 CorrectDerived::calculate(double)
return 0;
}
预期输出:
===== 无 override 保护(隐藏而非覆盖) =====
WrongDerived::process(unsigned) 10
WrongDerived::process(unsigned) 10
Base::process(int) = 10
===== 有 override 保护(正确覆盖) =====
CorrectDerived::process(int) = 10
CorrectDerived::calculate(double) = 3.14
逐段分析:
WrongDerived::process(unsigned)没有override——参数类型不同 (intvsunsigned int),它是隐藏而非覆盖- 当通过
Base*调用process(10)时,由于没有覆盖,调用的是Base::process(int)——完全不是预期行为 CorrectDerived使用override——如果签名不匹配直接编译错误,强制开发者正确覆盖- 这就是为什么所有虚函数覆盖都必须用
override——这是 C++11 最重要的代码安全改进之一
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 虚函数未加 override | 签名不匹配时隐藏而非覆盖 | 所有覆盖都加 override |
| 构造函数中调用虚函数 | 调用基类版本而非派生类版本 | 构造函数中避免调用虚函数 |
| 析构函数中调用虚函数 | 同样只调用当前类版本 | 析构函数中避免调用虚函数 |
| final 类被继承 | 编译错误 | 检查设计意图 |
| 协变返回类型使用不当 | 返回类型不兼容 | 指针/引用类型才支持协变 |
常见面试问题
虚函数和普通函数的本质区别?实现原理?——虚函数通过虚函数表(vtable)实现运行时多态,调用开销略高于普通函数(间接跳转)。普通函数在编译时静态绑定。
override和final的作用?——override标记虚函数覆盖,编译器验证基类确有匹配的虚函数。final阻止虚函数被进一步覆盖或类被继承。构造函数和析构函数中调用虚函数的行为?——调用的是当前正在构造/析构的类的版本,而不是最终派生类的版本。因为派生类部分尚未构造或已被析构。
什么是协变返回类型?——派生类中覆盖虚函数的返回类型可以是基类虚函数返回类型的派生类指针或引用(
Base* → Derived*)。虚函数可以是 static 的吗?——不能。static 成员函数没有
this指针,无法动态绑定。
小结
- 虚函数通过 vtable 实现运行时多态——基类指针/引用调用时动态分派到实际类型
- 所有虚函数覆盖必须加
override——这是 C++11 最重要的编码规范之一 final阻止虚函数被覆盖或类被继承——在不需要扩展的地方加锁- 构造函数/析构函数中避免调用虚函数——此时动态绑定被禁用
- 多态 = 统一接口 + 不同行为,是面向对象设计的核心价值