目录
[1. 多态的概念](#1. 多态的概念)
[1.1 静态多态(编译时多态)](#1.1 静态多态(编译时多态))
[1.2 动态多态(运行时多态)](#1.2 动态多态(运行时多态))
[2. override和final](#2. override和final)
[2.1 override:显式检测虚函数重写](#2.1 override:显式检测虚函数重写)
[2.2 final:禁止重写或继承](#2.2 final:禁止重写或继承)
[3. 多态的原理(vptr、vtable)](#3. 多态的原理(vptr、vtable))
[4. 含虚函数的对象的大小](#4. 含虚函数的对象的大小)
[5. 动态绑定与静态绑定](#5. 动态绑定与静态绑定)
[6. 经典面试题](#6. 经典面试题)
[7. 析构函数的重写](#7. 析构函数的重写)
[8. 协变](#8. 协变)
[9. 纯虚函数和抽象类](#9. 纯虚函数和抽象类)
[10. 虚函数表末尾的标记(了解)](#10. 虚函数表末尾的标记(了解))
1. 多态的概念
在C++中,多态(polymorphism)是面向对象编程的三大特性之一,核心思想是"一个接口,多种实现"------即同一操作作用于不同对象时,可产生不同的行为。
C++中的多态分为两类:静态多态 (编译时多态)和动态多态(运行时多态)。
1.1 静态多态(编译时多态)
静态多态是在编译时确定具体调用的函数,主要通过以下几种方式实现:
**函数重载:**同一作用域内,函数名相同但参数列表(参数类型、个数、顺序)不同的函数,编译器会根据实参匹配对应的函数。
void print(int a) { cout << "int: " << a << endl; }
void print(double b) { cout << "double: " << b << endl; }
void print(string s) { cout << "string: " << s << endl; }
int main() {
print(10); // 调用print(int)
print(3.14); // 调用print(double)
print("hello"); // 调用print(string)
return 0;
}
编译器在编译时通过实参的静态类型,匹配最符合的函数,生成直接调用该函数的代码(静态绑定)。
运算符重载:对内置运算符(如+、<<、[ ] 等)进行重新定义,使其能用于自定义类型(如类对象),编译器会根据操作数类型匹配对应的重载版本。
class Student
{
public:
string name = "LING";
//类对象与整数相加
int operator+ (int m)
{
return this->age + m;
}
private:
int age = 18;
};
int main()
{
Student obj1;
int val = obj1 + 5; // int val = obj1.operator+ (5)
cout << "类对象与整数相加: " << val << endl;
return 0;
}
编译器将obj1 + 5转换为 obj1.operator+ (5),并根据操作数的静态类型匹配对应的运算符重载函数(静态绑定)。
**模版:**通过类型参数定义通用的函数或类,编译器根据实际传入的类型参数,在编译时生成具体的函数/类的实例,从而实现对不同类型的通用操作。
函数模版
// 通用交换函数模板
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x=1, y=2;
swap(x, y); // 编译时实例化 swap<int>(int&, int&)
double a=3.14, b=6.28;
swap(a, b); // 编译时实例化 swap<double>(double&, double&)
return 0;
}
类模版
// 通用的"加法器"类模板
template <typename T>
class Adder {
public:
T add(T a, T b) { return a + b; } // 依赖T支持+运算符
};
int main() {
Adder<int> intAdder;
cout << intAdder.add(1, 2) << endl; // 3(int的+)
Adder<string> strAdder;
cout << strAdder.add("hello", "world") << endl; // "helloworld"(string的+)
return 0;
}
模版是"代码生成器",编译器根据传入的类型参数,生成针对该类型的具体函数或具体类的代码。由于操作在编译时确定,因此属于静态多态。
1.2 动态多态(运行时多态)
动态多态是在程序运行阶段 才确定具体调用的函数,主要通过虚函数和继承实现。
实现动态多态必须满足以下3个条件:
- **存在继承关系:**有基类和派生类;
- 派生类重写基类的虚函数:派生类的函数与基类虚函数的函数名、参数列表、返回值完全相同(协变返回类型除外);
- 通过基类指针或引用调用虚函数:只有通过基类的指针或引用,才能触发运行时的函数绑定。
**虚函数:**基类中用virtual关键字声明的函数就是虚函数,用于被子类重写。**重写(Override):**派生类对基类虚函数的重新实现。
注:派生类重写时可以省略virtual,但最好加上,提高可读性。
多态的核心是动态绑定,当通过基类指针/引用调用虚函数时,函数调用不再根据对象的静态类型调用,而是由其实际指向的动态类型决定("指向谁,调用谁")。
class Person
{
public:
virtual void BuyTicket() //虚函数
{ cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() //重写基类虚函数
{ cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
class Animal
{
public:
virtual void talk() const //虚函数
{}
};
class Dog : public Animal
{
public:
virtual void talk() const //重写基类虚函数
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const //重写基类虚函数
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}
2. override和final
在C++中,虚函数重写(Override)的规则较为严格(需函数名、参数列表、返回值完全一致,协变返回除外),但手动保证"正确重写"容易出错。C++11引入override和final关键字,用于增强虚函数重写的安全性与明确性。
2.1 override:显式检测虚函数重写
告诉编译器"当前函数是重写基类的虚函数"。编译期检查时,若该函数并未真正重写基类虚函数(如函数名拼写错误、参数类型/个数不匹配),编译器会直接报错,避免运行时才发现错误。
class Base
{
public:
virtual void func(int x) { /* ... */ }
};
class Derived : public Base
{
public:
// 编译报错:Base中没有"virtual void func(double x)"的虚函数可重写
void func(double x) override { /* ... */ }
};
2.2 final:禁止重写或继承
final有两种用法:
(1)禁止该类被继承(已经在前面继承章节讲到,这里就不提了)。
(2)禁止派生类重写该虚函数函数。
class Base
{
public:
// final标记:该虚函数不能被派生类重写
virtual void func() final { /* ... */ }
};
class Derived : public Base
{
public:
// 编译报错:Base::func() 已被final禁止重写
void func() override { /* ... */ }
};
3. 多态的原理(vptr、vtable)
C++多态的底层实现依赖虚函数表 (Virtual Table,简称vtable)和虚表指针(Virtual Pointer,简称vptr),核心是运行时动态绑定。
虚函数表(vtable):类级别的函数地址"索引表"
**本质:**每个包含虚函数的类,编译器会为其生成一张虚函数表(本质是"函数指针数组"),用于存储该类所有虚函数的地址。
特点:
- 每个包含虚函数的类只有一张虚函数表(所有同类型的对象共享同一张虚函数表),在编译时生成,存储在全局数据区。
- 若派生类重写了基类的虚函数,派生类的虚函数表中,对应基类虚函数 的位置会被覆盖为派生类重写后的函数地址;
- 若派生类有新增虚函数,则这些函数的地址会被追加到派生类虚函数表的末尾。
虚表指针(vptr):对象级别的表指针,对象与虚函数表的"纽带"
本质: 每个包含虚函数的类对象中,会隐含一个指向"当前对象所属类的虚函数表指针"(通常位于对象内存的起始位置,由编译器自动添加)。
特点: 每个包含虚函数的类的对象的虚表指针vptr,在对象创建时被初始化,指向该对象所属类的虚函数表。
示例1
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person
{
public:
virtual void BuyTicket() //重写虚函数BuyTicket
{ cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier : public Person
{
public:
virtual void BuyTicket() //重写虚函数BuyTicket
{ cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
派生类Student和Soldier继承Person后,会生成各自的虚函数表 ,由于Student和Soldier重写了BuyTicket,它们的虚函数表中,BuyTicket的地址会被替换为自身的Student: :BuyTicket或Soldier: :BuyTicket的地址;
包含虚函数的类创建对象时,对象的vptr会被初始化为指向所属类的虚函数表(如创建Person ps 时, ps的vptr会被初始化为指向Person类的虚函数表)
调试观察:

多态调用完整流程(以调用Func(&st) ,最终输出买票--打折为例):
**编译时:**编译器发现BuyTicket是虚函数,且通过指针ptr调用,会触发动态绑定,因此不会直接确定函数地址,而是生成"通过vptr查找vtable"的代码。
运行时:
- 通过指针ptr找到它所指向的Student对象(动态类型对应的对象);
- 从Student对象中取出vptr;
- 通过vptr找到Student类的vtable,在表中查找BuyTicket()对应的函数地址(此时地址是Student::func()的地址,因为派生类重写后覆盖基类版本)。
- 调用找到的函数地址,最终执行Student::func(),实现多态。
这就是多态的底层实现:通过虚函数表和虚指针,在运行时根据对象实际类型决定调用哪个函数。
示例2
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
};
class Derived : public Base
{
public:
void func1() override { cout << "Derived::func1" << endl; } // 重写func1
virtual void func3() { cout << "Derived::func3" << endl; } // 新增虚函数func3
};
int main()
{
Base b;
Derived d;
Base* ptr = new Derived();
ptr->func1();
return 0;
}
调试观察:
基类Base的虚函数表结构(声明了两个虚函数):
0\]:Base::func1()的函数地址。
派生类Derived的虚函数表结构(Derived继承了Base,并重写了func1,新增了虚函数func3()):
0\]:Derived::func1()的函数地址(重写后,覆盖了基类Base::func1()的地址)。
2\]:Derived::func3()的函数地址(新增虚函数,追加到表的末尾,这里调试中观察不到)。
编译时:编译器根据指针类型Base*,检查Base中是否有func1(存在且是虚函数),但不决定具体调用哪个版本。
运行时:
通过ptr找到Derived对象的虚表指针(__vfptr);
由__vfptr找到Derived类的虚函数表;
在表中找到func1的地址(因派生类重写,此处是Derived::func1()的地址);
调用该地址的函数,实现"运行时多态"。
结论:
派生类重写基类虚函数时,虚函数表中对应位置的地址会被修改为派生类的实现 (如Derived::func1覆盖Base::func1)。
未重写的虚函数,派生类虚函数保留基类的函数地址 (如Derived::vtable[1]仍为Base::func2)。
派生类如果有新增的虚函数,这些虚函数的地址会被添加到派生类虚函数表的末尾(调试中观察不到,逻辑上func3对应[2])。
4. 含虚函数的对象的大小
下面在32位程序下的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
含有虚函数的类,其对象会包含一个虚表指针(vptr),用于指向类的虚函数表(vtable)。在32位程序中,指针的大小为4字节。int是4字节,char是1字节,为满足内存对齐补3个字节。计算:vptr(4)+int(4)+char(1)+填充(3)=12字节。
5. 动态绑定与静态绑定
静态类型: 变量或表达式在编译阶段就确定的类型,由声明时的类型直接决定,不会随程序运行而改变。
动态类型: 变量或表达式在运行阶段实际指向的对象类型,即"真实的对象类型",运行时确定,可能随程序运行而改变(例如指针重新指向不同类型的对象)。
class Base {};
class Derived : public Base {};
int main() {
Base b; // 静态类型:Base(声明时确定)
Derived d; // 静态类型:Derived(声明时确定)
Base* ptr = &d; // 静态类型:Base*(指针声明为Base*,与指向的对象无关)动态类型:Derived*(实际指向Derived对象)
ptr = &b; // 动态类型变为Base* (重新指向Base对象)
Base& ref = d; // 静态类型:Base&(引用声明为Base&,与引用的对象无关)动态类型:固定为 Derived&(实际引用Derived对象)
return 0;
}
静态绑定
对于非虚函数,编译器是根据静态类型来调用的。
规则:编译器根据变量的静态类型 确定调用哪个函数,在编译时就已确定调用关系。
class Base {
public:
void func() { cout << "Base::func()" << endl; } // 非虚函数
};
class Derived : public Base {
public:
void func() { cout << "Derived::func()" << endl; } // 非虚函数(隐藏基类版本)
};
int main() {
Base* ptr = new Derived();
ptr->func(); // 调用哪个?
return 0;
}
ptr的静态类型是Base*,非虚函数func采用静态绑定。编译时,编译器直接根据静态类型Base*确定调用Base::func(),输出Base::func()。
动态绑定
对于虚函数,编译器是根据动态类型来调用的。
规则:编译器根据变量的动态类型确定调用哪个函数,在运行时才确定调用关系。
class Base {
public:
virtual void func() { cout << "Base::func()" << endl; } // 虚函数
};
class Derived : public Base {
public:
void func() override { cout << "Derived::func()" << endl; } // 重写虚函数
};
int main() {
Base* ptr = new Derived();
ptr->func(); // 调用哪个?
return 0;
}
ptr的静态类型是Base*,当动态类型是Derived*,虚函数采用动态绑定。运行时,程序通过ptr指向的Derived对象的虚表指针,找到Derived的虚函数表,调用Derived::func(),输出Derived::func()。
编译器的执行过程:
编译时:
第一步:检查被调用的函数是否为虚函数
- 若函数不是虚函数,直接采用静态绑定 。编译器根据调用者的静态类型,在编译阶段就确定要调用的函数地址,生成"直接调用该地址函数"的机器码。
- 若是虚函数:进行下一步,检查调用方式。
第二步:检查调用方式
- 如果通过对象本身调用(如d.func(),d是Derived类型的对象),对象的静态类型与动态类型完全一致,生成"直接调用该地址函数"的机器码 (本质是静态绑定)。
- 如果通过基类的指针或引用调用(如ptr->func(),ptr是Base*类型),编译器无法在编译时确定指针/引用的动态类型(可能指向Base或Derived),因此会生成动态绑定调用逻辑的机器码(即①从指针/引用指向的对象中取出vptr②通过vptr找到所属类的vtable③在vtable中找到目标函数的地址④调用该函数)
运行时:
- **静态绑定:**编译时生成的机器码是"直接跳转至固定函数地址"的指令(例如call 0x123456),运行时,CPU直接执行这条命令,跳转到编译时确定的函数地址。
- **动态绑定:**编译时生成的机器码是"通过vptr查找vtable并获取函数地址"的逻辑,运行时,执行机器码,最终调用的函数地址由运行时的vtable决定。
ptr是指针+func()是虚函数满足多态条件,实现动态绑定,汇编层:

func()不是虚函数,不满足多态条件,实现静态绑定,编译器直接确定调用函数地址,汇编层:

6. 经典面试题
下面程序的输出结果是什么()
A. A->0 B. B->1 C. A->1 D. B->0 E. 编译出错 D. 以上都不正确
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
//p->func(); 输出B->0
return 0;
}
重写的核心规则:"签名一致,替换实现",函数重写实际上只重写了函数体,没有重写函数声明中的参数。
函数的参数没有被重写的原因:**接口一致性:**重写的目的是"改变函数的行为",而函数的参数规则(包括默认参数)被视为"接口规范"的一部分,应由基类定义(避免派生类随意修改参数规则导致接口混乱)。
**效率:**默认参数在编译时即可确定,无需像函数体那样动态查找,否则会增加运行时开销。
解析这道题之前,我们需要了解的规则:
在C++中,如果函数的所有参数都指定了默认值,那么当遇到无参调用时,编译器会在编译阶段自动补充默认参数。
void func(int a=1) {} func()//编译器自动补全func(1) void func(int a = 1, int b = 2) {} func()//编译器自动补全func(1,2)
编译器在编译阶段就会把 "无参调用" 替换成 "带默认值的调用"。
默认参数遵循静态绑定规则,若调用时未传参,默认参数是编译器在编译时,根据 "调用者的静态类型" 从类声明中直接读取的。this指针的静态类型,等于函数所属的类的类型;this指针的动态类型,等于调用该函数的实际对象类型。
解析:
p->test():
- test()是虚函数,p动态类型是B*--->根据动态类型B的vptr找到vatble--->由于B类没有重写test(),所以vatble中的test()指针还是指向A::test()
- A::test()内this->func()--->this的静态类型是A*(test是A类的成员函数)--->所以默认参数采用A::func的val=1---> this的动态类型是B*--->func是虚函数,this的动态类型是B*,根据动态类型找到B类重写的func()函数--->执行B::func(1)--->输出B->1。
p->func():
- p的静态类型是B*--->编译时采用val=0 补全B的默认参数--->func是虚函数,p的动态类型是B*--->根据动态类型找到B类重写的func()函数---> 执行B::func(0)--->输出B->0。
总结:虚函数的实现看实际对象类型(动态绑定),默认参数看编译时静态类型(静态绑定)。
7. 析构函数的重写
为什么基类中的析构函数建议设计成虚函数呢?
//基类
class Base
{
public:
Base() { cout << "Base 构造" << endl; }
// 非虚析构(错误示范)
// ~Base() { cout << "Base 析构" << endl; }
// 虚析构(正确做法)
virtual ~Base() { cout << "Base 析构" << endl; }
};
//派生类
class Derived : public Base
{
private:
int* data; //派生类动态资源
public:
Derived() : data(new int(10)) { cout << "Derived 构造" << endl; }
~Derived() //派生类析构:先释放自身资源,再调用基类析构
{
delete data; //释放派生类资源
cout << "Derived 析构" << endl;
}
};
int main()
{
Base* ptr = new Derived(); //基类指针指向派生类对象
delete ptr; //销毁对象
return 0;
}
当我们通过delete ptr销毁对象时,程序的行为取决于基类析构函数是否为虚函数:
若~Base()是非虚函数 :析构函数调用采用"静态绑定",根据静态类型Base*,只调用~Base(),**派生类的~Derived()未被调用,导致data指向的动态内存未被释放,造成内存泄漏。**运行结果:
若~Base()是虚函数:满足动态多态,析构函数调用采用"动态绑定",根据动态类型Derived*,先调用~Derived()释放派生类资源,再调用~Base()释放基类资源,符合预期。运行结果:
为了让析构函数支持多态(从而保证资源被正确释放),编译器必须特殊处理析构函数的名称,使其满足"虚函数重写"的条件。基类析构函数名称和派生类析构函数名称不可能相同,**所以析构函数的名称被统一处理为destructor,**为了让析构函数满足"虚函数重写"的要求,从而支持多态,保证对象能按"派生类->基类"的顺序正确析构,避免资源泄漏。
所以当基类析构函数不是虚函数时,派生类析构函数与基类析构函数因编译后名称统一为destructor,会构成"函数隐藏"关系(继承章节讲到)。
派生类析构无需显式virtual,只要基类析构是虚函数,派生类析构自动为虚函数。
如果一个类明确不会被继承,或不会通过基类指针/引用销毁派生类对象,那么基类析构函数可以不是虚函数,但在实际开发中,往往需要考虑"可继承性",所以建议基类析构函数设计为虚函数。
8. 协变
在C++中,协变是虚函数重写的一种特殊规则,当基类函数返回基类的指针或引用时,派生类重写该虚函数时,可以返回派生类的指针或引用,这种情况称为协变返回类型。
协变的条件:
- 基类虚函数的返回值必须是基类类型的指针或引用;
- 派生类重写的返回值必须是派生类类型的指针或引用;
class Base
{
public:
// 基类虚函数,返回 Base*
virtual Base* clone() const
{
return new Base(*this); // 复制自身(基类对象)
}
virtual ~Base() {} // 虚析构,避免内存泄漏
};
class Derived : public Base
{
public:
// 派生类重写 clone,返回 Derived*(协变)
Derived* clone() const override
{
return new Derived(*this); // 复制自身(派生类对象)
}
};
int main()
{
Base* b = new Derived();
Base* bClone = b->clone(); //调用 Derived::clone(),返回 Derived*(协变允许),隐式转换为 Base*
delete bClone;
delete b;
return 0;
}
9. 纯虚函数和抽象类
纯虚函数
纯虚函数是基类中声明的特殊虚函数 ,它没有具体的函数体 ,通过在虚函数声明末尾添加**=0**标识(纯虚函数的"=0"表示"无实现")。
**作用:**纯虚函数的核心作用是强制派生类必须实现该函数,从而实现多态。
抽象类
包含纯虚函数的类称为抽象类。
抽象类的特点:
- 抽象类不能创建对象,仅作为接口基类。
- 派生类必须重写所有虚函数才能实例化,否则派生类也是抽象类。
// 抽象类:包含纯虚函数draw()
class Shape {
public:
// 纯虚函数:强制派生类实现绘制逻辑
virtual void draw() = 0;
// 普通虚函数(可选):可提供默认实现
virtual void printInfo() {
cout << "This is a shape." << endl;
}
// 析构函数通常声明为虚函数(避免多态销毁时内存泄漏)
virtual ~Shape() {}
};
// 派生类:Circle,必须重写draw()
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 重写纯虚函数draw()
void draw() override {
cout << "绘制一个半径为" << radius << "的圆" << endl;
}
// 重写普通虚函数(可选)
void printInfo() override {
cout << "这是一个圆,半径:" << radius << endl;
}
};
// 派生类:Rectangle,必须重写draw()
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() override {
cout << "绘制一个宽" << width << "、高" << height << "的矩形" << endl;
}
};
int main() {
// 错误:抽象类不能实例化
// Shape shape;
// 正确:通过派生类实例化
Shape* shapes[2] = {
new Circle(5.0),
new Rectangle(3.0, 4.0)
};
// 多态调用:根据动态类型执行对应派生类的draw()
for (int i = 0; i < 2; i++) {
shapes[i]->draw();
shapes[i]->printInfo();
delete shapes[i];
}
return 0;
}
纯虚函数与普通虚函数的区别:
- 普通虚函数有默认实现,派生类可以选择重写或继承。
- 纯虚函数无默认实现,派生类必须重写。
10. 虚函数表末尾的标记(了解)
C++ 标准从未规定虚函数表的具体结构(包括是否需要结束标记),这属于编译器的 "未定义行为",不同厂商的实现可以不同:
- VS系列编译器:会在 vtable 末尾添加一个 NULL指针0x00000000作为结束标记。
- GCC(g++)系列编译器:通常不添加结束标记
VS可自行调试观察虚函数表的末尾。