Limesn
Limesn
发布于 2026-09-28 / 1 阅读
0

继承与多态:一次看懂构造、析构、虚函数与绑定规则

一、先记住四句话

  1. 构造顺序:基类 → 派生类;析构顺序:派生类 → 基类。
  2. 非虚函数看静态类型;虚函数看动态类型。
  3. 构造函数和析构函数中调用虚函数,不会向下分派到更派生的类。
  4. 基类析构函数最好声明为 virtual。

二、静态类型 vs 动态类型

A *pa = new C();
  • pa 的静态类型:A*,也就是声明类型。
  • pa 的动态类型:C,也就是实际指向的对象类型。

规则:

  • 调用非虚函数:看静态类型。
  • 调用虚函数:看动态类型。
  • 在构造/析构函数里调用虚函数:特殊,不会看完整动态类型。

三、构造与析构顺序

如果有:

class A {};
class B : public A {};
class C : public B {};

创建 C 对象时:

A 构造
B 构造
C 构造

销毁 C 对象时:

C 析构
B 析构
A 析构

一句话:构造从基类到派生类,析构从派生类到基类。


四、虚函数与多态

多态发生条件:

  1. 通过指针或引用调用;
  2. 调用的是虚函数;
  3. 派生类真正重写了它。
A *pa = new C();
pa->f();   // 如果 f 是虚函数,调用 C::f()
pa->g();   // 如果 g 非虚,调用 A::g()

记忆:

非虚看左边,虚函数看右边。
左边是指针类型,右边是实际对象。


五、重写、隐藏、重载

名称含义
重载同一个类里,同名不同参数
重写派生类重新实现基类的虚函数,签名一致
隐藏派生类有同名函数,但基类函数不是虚函数,或签名不同

例如:

A::FuncA() 不是虚函数
B::FuncA() 只是隐藏了 A::FuncA()

所以通过 A* 调用 FuncA(),永远进 A::FuncA()。


六、构造函数/析构函数中调用虚函数

在构造函数中,对象还没构造完整;在析构函数中,派生部分已经销毁。
所以此时虚函数不会分派到更派生的类。

例如:

B::B() {
    f();   // 此时 C 还没构造,不会调用 C::f()
}

如果 B 没有重写 f(),就调用 A::f()。


七、虚析构函数

A *pa = new C();
delete pa;

如果 ~A() 不是虚函数,只会调用 A 的析构,C、B 的析构不会调用。
如果 ~A() 是虚函数,才会正确调用:

C 析构
B 析构
A 析构

所以基类析构函数最好写成:

virtual ~A() {}

八、例 1 分析

关键代码:

class A {
public:
    A() { cout << "A's cons." << endl; }
    virtual ~A() { cout << "A's des." << endl; }
    virtual void f() { cout << "A's f()." << endl; }
    void g() { f(); }
};

class B : public A {
public:
    B() {
        f();
        cout << "B's cons." << endl;
    }
    ~B() { cout << "B's des." << endl; }
};

class C : public B {
public:
    C() { cout << "C's cons." << endl; }
    ~C() { cout << "C's des." << endl; }
    void f() { cout << "C's f()." << endl; }
};

int main() {
    A *pa = new C();
    pa->g();
    delete pa;
}

输出:

A's cons.
A's f().
B's cons.
C's cons.
C's f().
C's des.
B's des.
A's des.

逐步看:

  1. new C():构造顺序 A → B → C。
  2. A 构造:输出 A's cons.
  3. B 构造:进入 B::B(),调用 f()。
    此时 C 还没构造,不会调用 C::f()。
    B 没有重写 f(),所以调用 A::f(),输出 A's f().
    然后输出 B's cons.
  4. C 构造:输出 C's cons.
  5. pa->g():g() 是 A 的非虚函数,但里面调用虚函数 f()。
    此时对象已完整,动态类型是 C,所以调用 C::f(),输出 C's f().
  6. delete pa:~A() 是虚函数,析构顺序 C → B → A,输出:
    C's des.
    B's des.
    A's des.
    

九、例 2 分析

关键代码:

class A {
public:
    void FuncA() { printf("FuncA called\n"); }
    virtual void FuncB() { printf("FuncB called\n"); }
};

class B : public A {
public:
    void FuncA() {
        A::FuncA();
        printf("FuncAB called\n");
    }
    virtual void FuncB() { printf("FuncBB called\n"); }
};

int main() {
    B b;
    A *pa;
    pa = &b;
    A *pa2 = new A;

    pa->FuncA();
    pa->FuncB();
    pa2->FuncA();
    pa2->FuncB();

    delete pa2;
}

输出:

FuncA called
FuncBB called
FuncA called
FuncB called

逐步看:

  1. B b;:创建 B 对象,无输出。
  2. pa = &b;:pa 静态类型 A*,动态类型 B。
  3. pa->FuncA();
    • FuncA 不是虚函数,静态绑定。
    • 看 pa 的静态类型 A*,调用 A::FuncA()。
    • 输出:FuncA called
  4. pa->FuncB();
    • FuncB 是虚函数,动态绑定。
    • pa 实际指向 B,调用 B::FuncB()。
    • 输出:FuncBB called
  5. pa2->FuncA();
    • pa2 指向真正的 A 对象。
    • FuncA 非虚,调用 A::FuncA()。
    • 输出:FuncA called
  6. pa2->FuncB();
    • FuncB 虚函数,动态绑定。
    • pa2 实际指向 A,调用 A::FuncB()。
    • 输出:FuncB called
  7. delete pa2;
    • 删除的是 A 对象,A 没有显式析构,无输出。

注意:B::FuncA() 在这个 main 中从未被调用。
如果通过 B 对象或 B* 调用 FuncA(),才会进入 B::FuncA(),输出:

FuncA called
FuncAB called

十、速查表

调用是否虚函数绑定方式看谁结果
pa->FuncA()否静态绑定A*A::FuncA()
pa->FuncB()是动态绑定实际对象 BB::FuncB()
pa2->FuncA()否静态绑定A*A::FuncA()
pa2->FuncB()是动态绑定实际对象 AA::FuncB()

最后总结

  • 构造:基类先,派生类后。
  • 析构:派生类先,基类后。
  • 非虚函数:看静态类型。
  • 虚函数:看动态类型。
  • 构造/析构中调用虚函数:不会向下分派。
  • 基类析构最好 virtual。

记住这几条,继承和多态的题基本都能拆开看。