一、先记住四句话
- 构造顺序:基类 → 派生类;析构顺序:派生类 → 基类。
- 非虚函数看静态类型;虚函数看动态类型。
- 构造函数和析构函数中调用虚函数,不会向下分派到更派生的类。
- 基类析构函数最好声明为
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 析构
一句话:构造从基类到派生类,析构从派生类到基类。
四、虚函数与多态
多态发生条件:
- 通过指针或引用调用;
- 调用的是虚函数;
- 派生类真正重写了它。
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.
逐步看:
new C():构造顺序A → B → C。A构造:输出A's cons.B构造:进入B::B(),调用f()。
此时C还没构造,不会调用C::f()。
B没有重写f(),所以调用A::f(),输出A's f().
然后输出B's cons.C构造:输出C's cons.pa->g():g()是A的非虚函数,但里面调用虚函数f()。
此时对象已完整,动态类型是C,所以调用C::f(),输出C's f().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
逐步看:
B b;:创建B对象,无输出。pa = &b;:pa静态类型A*,动态类型B。pa->FuncA();FuncA不是虚函数,静态绑定。- 看
pa的静态类型A*,调用A::FuncA()。 - 输出:
FuncA called
pa->FuncB();FuncB是虚函数,动态绑定。pa实际指向B,调用B::FuncB()。- 输出:
FuncBB called
pa2->FuncA();pa2指向真正的A对象。FuncA非虚,调用A::FuncA()。- 输出:
FuncA called
pa2->FuncB();FuncB虚函数,动态绑定。pa2实际指向A,调用A::FuncB()。- 输出:
FuncB called
delete pa2;- 删除的是
A对象,A没有显式析构,无输出。
- 删除的是
注意:B::FuncA() 在这个 main 中从未被调用。
如果通过 B 对象或 B* 调用 FuncA(),才会进入 B::FuncA(),输出:
FuncA called
FuncAB called
十、速查表
| 调用 | 是否虚函数 | 绑定方式 | 看谁 | 结果 |
|---|---|---|---|---|
pa->FuncA() | 否 | 静态绑定 | A* | A::FuncA() |
pa->FuncB() | 是 | 动态绑定 | 实际对象 B | B::FuncB() |
pa2->FuncA() | 否 | 静态绑定 | A* | A::FuncA() |
pa2->FuncB() | 是 | 动态绑定 | 实际对象 A | A::FuncB() |
最后总结
- 构造:基类先,派生类后。
- 析构:派生类先,基类后。
- 非虚函数:看静态类型。
- 虚函数:看动态类型。
- 构造/析构中调用虚函数:不会向下分派。
- 基类析构最好
virtual。
记住这几条,继承和多态的题基本都能拆开看。