virtual 关键字与多态(第 13 章)

← 返回虚函数 | ← 主页


目录


🔴静态联编 vs 动态联编

🔺联编:将函数调用与具体函数体绑定的过程

🔺静态联编(无 virtual):编译期根据指针/引用的类型决定调用哪个函数

class Animal {
public:
    void speak() { cout << "...\n"; }
};
class Dog : public Animal {
public:
    void speak() { cout << "Woof\n"; }
};
 
Animal* p = new Dog();
p->speak();  // 输出 "..."  ← 看指针类型 Animal*,调用 Animal::speak

🔺动态联编(有 virtual):运行期根据对象的实际类型决定调用哪个函数

class Animal {
public:
    virtual void speak() { cout << "...\n"; }
};
class Dog : public Animal {
public:
    virtual void speak() { cout << "Woof\n"; }
};
 
Animal* p = new Dog();
p->speak();  // 输出 "Woof" ← 看对象实际类型 Dog,调用 Dog::speak

🔴virtual 的用法

🔺在基类函数声明前加 virtual,派生类中该函数自动也是虚函数(不需要重复写,但建议加 override

class Base {
public:
    virtual void func();   // 声明为虚函数
};
 
class Derived : public Base {
public:
    void func() override;  // override 让编译器检查是否真的重写了基类虚函数
};

🔺override 的作用:若函数签名写错(原基类无重写的这个函数),编译器会报错,避免静默地创建新函数而非重写

class Derived : public Base {
public:
    void func(int x) override;  // ❌ 编译错误:基类没有 func(int),不是重写
};

🔺重写(override)的条件:函数名、参数列表、const 属性必须完全一致,返回类型通常也要一致(协变返回类型除外)


🔴虚函数表 vtable

🔺每个含虚函数的类有一张虚函数表(vtable),每个对象有一个隐藏的**虚指针(vptr)**指向该表

Dog 对象内存布局:
┌─────────┐
│  vptr   │ ──→ Dog::vtable ──→ [ &Dog::speak, &Dog::eat, ... ]
├─────────┤
│  数据   │
└─────────┘

Animal* p = new Dog();
p->speak();
// 运行时:通过 vptr 找到 Dog::vtable,再找到 Dog::speak 并调用

🔺开销:每个对象多一个指针大小(通常 8 字节);每次虚函数调用多一次间接寻址


🔴虚析构函数

🔺通过基类指针 delete 派生类对象时,若析构函数不是虚函数,只调用基类析构函数 → 派生类资源泄漏

class Base {
public:
    ~Base() { cout << "Base 析构\n"; }  // 非虚
};
class Derived : public Base {
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() { delete[] data; }       // 不会被调用!
};
 
Base* p = new Derived();
delete p;  // 只调用 ~Base(),~Derived() 未调用 → 内存泄漏

🔺解决:基类析构函数声明为 virtual

class Base {
public:
    virtual ~Base() {}  // ⭐ 只要类会被继承,析构函数就应该是虚函数
};

🔴virtual 的限制

🔺构造函数不能是虚函数:构造时对象还未完全建立,vptr 尚未初始化,虚机制无法工作

🔺静态成员函数不能是虚函数:静态函数没有 this 指针,不属于任何对象实例

🔺友元函数不能是虚函数:友元不是成员函数

🔺inline 函数声明为 virtual 通常无意义:虚函数在运行期动态绑定,inline 在编译期展开,两者矛盾


本节小结

要点说明
静态联编无 virtual,编译期按指针类型绑定
动态联编有 virtual,运行期按对象实际类型绑定
override让编译器验证重写是否正确
vtable/vptr每个含虚函数的类有虚表,每个对象有虚指针
虚析构函数基类析构必须是 virtual,否则派生类析构不会被调用
不能是虚函数构造函数、静态成员函数、友元函数