多态
1.多态的定义及实现
多态指同一行为呈现多种形态,分 编译时(静态)和运行时(动态) 两类。
编译时多态靠函数重载和模板实现:同一函数名,因参数类型、数量不同,编译时就确定调用哪个版本,比如print(int)和print(string)会被区分。
运行时多态则通过继承和虚函数实现:基类指针指向派生类对象时,调用虚函数会根据实际对象类型,在程序运行时确定执行派生类还是基类的实现,比如Animal指针调用eat(),实际执行Cat或Dog的具体方法。
两者核心区别在确定调用版本的时机:前者在编译期,后者在运行期。
1.1构成条件
多态是一个继承关系下的类对象去调用同一函数,产生了不同的行为。
两个重要条件:
- 存在基类和派生类的继承结构
- 派生类重写基类中的虚函数
- 通过基类的指针或引用调用虚函数

1.2虚函数
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。
- 虚函数声明允许派生类重写实现多态。
- 如果没有虚函数声明,派生类的同名函数不是重写,而是隐藏。
- C++11引入的
override关键字明确用于强制检查是否是重写了基类的虚函数,如果基类中不是虚函数会报错。 - 虚函数的核心作用是支持多态,而不是控制是否允许重写。
- 虚函数的具有传递性。当基类声明了虚函数,派生类重写该函数时,即使不显式添加
virtual关键字,该函数仍然会被视为虚函数,并且可以被进一步的派生类继续重写。 - 函数被声明为 virtual,启用了动态绑定机制,使得运行时能够根据对象的实际类型(B)来决定调用哪个版本的函数。
1.3动态绑定(查虚函数表)
实现原理:
- 虚函数表:
- 每个包含虚函数的类(或其派生类)会有一个虚函数表,这是一个存储函数指针的数组,记录了该类所有虚函数的地址。
- 基类和派生类各自维护自己的虚函数表。如果派生类重写了基类的虚函数,派生类的虚函数表中会替换为自身的函数地址;未重写的虚函数则沿用基类的地址。
- 虚指针:
- 每个包含虚函数的类的对象会隐含一个指向其类的虚函数表的指针(虚指针),通常在对象内存布局的最开始位置。
- 当创建对象时,虚指针会被自动初始化,指向所属类的虚函数表。
- 动态绑定过程:
- 当通过基类指针或引用调用虚函数时,程序会先通过对象的虚指针找到对应的虚函数表。
- 再根据函数在虚函数表中的索引找到具体的函数地址,最终调用该地址对应的函数(可能是基类或派生类的实现)。
- 这个过程在运行时完成,因此称为动态绑定(或运行时绑定)。
1.4函数重写
对基类虚函数进行重写才是真正的重写。
函数重写的本质是虚函数表中函数地址的动态替换。
编译器会为包含虚函数的类创建虚函数表(vtable),重写函数会替换派生类虚表中对应位置的函数地址。运行时通过对象的虚表指针(vptr)找到实际应调用的函数版本。
1.4.1函数重写中的协变
协变(Covariant) 是指派生类重写基类虚函数时,返回值类型可以与基类虚函数的返回值类型形成 “父子关系”(即派生类返回值类型是基类返回值类型的派生类),这种情况仍然被视为有效的重写。
即允许其他条件满足函数重写,返回值构成父派生类型,也是函数重写。但这样的返回值必须是指针或引用,值类型的是错误的。
#include <iostream>
// 基类
class Base {
public:
// 基类虚虚函数,返回Base*
virtual Base* clone() {
std::cout << "Base::clone()" << std::endl;
return new Base();
}
virtual ~Base() {} // 虚析构函数,确保正确析构
};
// 派生类
class Derived : public Base {
public:
// 重写clone(),返回Derived*(Base*的派生类指针)
Derived* clone() override { // 协变返回值
std::cout << "Derived::clone()" << std::endl;
return new Derived();
}
};
int main() {
Base* b = new Derived();
Base* copy = b->clone(); // 调用Derived::clone(),返回Derived*(隐式转换为Base*)
delete copy;
delete b;
return 0;
}
1.4.2析构函数的重写
基类析构函数必须声明为虚函数,此时派生类析构函数就会与基类析构函数构成重写。
虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成重写。
这就解释了派生类B中显式调用基类A的析构函数~A()调不到,原因是两者构成隐藏,得加上类域。不过一般不这么做。
1.4.3 override 和 final 关键字
虚函数重写时函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时才会发现。C++11提供了override关键字,可以强制检测是否构成重写。
class Car {
public:
virtual void Drive() {}
};
class AE86 {
public:
virtual void Drive() override {cout << "漂一下啊" << endl;}
};
C++11提供的final关键字不仅可以声明一个类不再被允许继承
class Person final {};
还可以声明一个函数不再被允许重写
class Person {
public:
void func() final {}
};
class Student {
public:
void func() override {...} // 报错
};
1.5静态绑定(直接确定函数地址)
静态绑定又称编译时绑定,是指函数调用在程序编译阶段就确定了具体要执行的函数版本,而无需等到运行时再做决定。这种绑定方式依赖于编译器在编译时对函数调用的解析,主要基于调用者的类型(而非对象的实际类型)。
核心是编译器在编译阶段通过函数名、参数列表、调用者类型等信息,直接确定要调用的函数地址,并生成对应的机器码。由于绑定过程在编译时完成,运行时不会产生额外的开销(如查找虚函数表的操作)。
默认参数数值在编译时静态绑定,与虚函数动态绑定机制分离。
1.6选择题
以下程序输出结果是什么()
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
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(){
B*p = new B;
p->test();
p->func();
return 0;
}

-
虚函数表和虚指针的构建
虚函数表结构:
- A 类的虚函数表:
- func() → A::func() 地址
- test() → A::test() 地址
- B 类的虚函数表:
- func() → B::func() 地址 (重写)
- test() → A::test() 地址 (继承)
每个对象包含一个指向对应类虚函数表的虚指针(vptr)。
- A 类的虚函数表:
-
p->test()执行过程
B* p = new B;创建 B 对象,该对象包含指向 B 类虚函数表的虚指针。
p->test() 执行过程:
- 通过指针 p 访问对象的虚指针(vptr)
- 通过vptr找到 B 类的虚函数表
- 在虚函数表中查找 test() 对应的函数地址,发现是 A::test()
- 调用 A::test(),在该函数内部执行 func()
在**A::test()内部调用func()**时
-
仍然是通过当前对象(this指针)的虚指针进行查找
-
this指针指向的是 B 对象,所以使用 B 类的虚函数表
-
在 B 类虚函数表中查找到 func() 对应的是 B::func()
-
调用 B::func(),但使用编译时确定的默认参数值 1(来自 A::func() 的声明,原因是默认参数数值在编译时静态绑定,与虚函数动态绑定机制分离)
因此输出:
B->1 -
p->func()执行过程
- 通过指针p访问对象的虚指针(vptr)
- 通过vptr找到B类的虚函数表
- 在虚函数表中查找func()对应的虚函数地址,是B::func()
- 直接调用B::func(),使用B::func()声明中的默认参数值0
因此输出:
B->0 -
最终输出结果
B->1 B->0
1.7重载/重写/隐藏的对比

2.纯虚函数和抽象类
2.1纯虚函数
纯虚函数是在基本类中声明但没有具体实现的虚函数。
virtual 返回类型 func(参数列表) = 0;
- 纯虚函数通过在声明末尾添加
=0来标识 - 纯虚函数没有函数体,它的作用是为派生类提供一个必须实现的接口规范
- 包含纯虚函数的类被称为抽象类。
2.2抽象类
含有纯虚函数的类被称为抽象类,具有以下特点:
-
抽象类不能被实例化,即不能创建抽象类的对象。
class abstractClass { public: virtual void func() = 0; // 纯虚函数 }; // 错误:不能实例化抽象类 abstractClass obj; // 编译错误 -
抽象类的派生类必须实现所有纯虚函数才能称为非抽象类(可实例化)
class normalClass : public abstractClass { public: void func() override { cout << "实现纯虚函数" << endl; } }; // 正确:normalClass是具体类,可以实例化 normalClass obj; -
抽象类可以包含普通成员函数和成员变量
-
可以声明抽象类的指针或引用,用于实现多态。
// car.h
#include <string>
#include <iostream>
using namespace std;
class Car {
public:
virtual void run() const = 0;
virtual void stop() const = 0;
Car() = default;
Car(const Car& car) = default;
Car(const char* name, const char* carId, const int price, const int speed)
: _name(name), _carId(carId), _price(price), _speed(speed) {
}
virtual ~Car() = default;
protected:
string _name;
string _carId;
int _price = 0;
int _speed = 0;
};
class Bmw final : public Car {
public:
Bmw() = default;
Bmw(const char* name, const char* carId, const int price, const int speed)
: Car(name, carId, price, speed) {
}
Bmw(const Bmw& bmw) = default;
void run() const override {
cout << "Bmw is running" << endl;
}
void stop() const override {
cout << "Bmw is stoping" << endl;
}
~Bmw() override = default;
};
class Audi final : public Car {
public:
Audi() = default;
Audi(const char* name, const char* carId, const int price, const int speed)
: Car(name, carId, price, speed) {}
Audi(const Audi& audi) = default;
void run() const override {
cout << "Audi is running" << endl;
}
void stop() const override {
cout << "Audi is stoping" << endl;
}
~Audi() override = default;
};
void run(const Car& car) {car.run();} // 多态
// main.cpp
#include "car.h"
int main(){
const Car* car = new Bmw("Bmw", "BMW-X5", 1000000, 200);
const Audi audi("Audi", "AUDI-A6", 500000, 180);
car->run();
run(audi);
delete car;
return 0;
}

3.多态的原理
3.1虚函数表和虚函数表指针
下⾯编译为64位程序的运行结果是什么() B
A. 编译报错 B. 运行报错 C. 8 D. 12 E. 16
class Base {
public:
virtual ~Base() = default;
virtual void func() {
cout << "Base" << endl;
}
protected:
int _i = 0;
char _c = 'a';
};
int main(){
Base base;
cout << sizeof(base) << endl;
return 0;
}
-
成员变量的大小与对齐
int _i:4 字节(对齐要求为 4 字节)char _c:1 字节(对齐要求为 1 字节)
这两个成员变量本身共占用
4 + 1 = 5字节,但由于内存对齐规则:- 成员变量需按各自对齐值排列
- 整体需满足类的最大对齐要求(此处为虚函数表指针的 8 字节对齐)
因此,
_i和_c实际占用 8 字节(包含 3 字节填充,用于满足 8 字节对齐)。 -
虚函数表指针(vptr)
Base类中包含虚函数,类在实例化时,编译器会为对象添加一个指向虚函数表的指针。
这个指针的类型是函数指针数组指针,是一个指向虚函数表的指针。
虚函数表是一个数组,存放着类里面每个虚函数的函数指针。
64位下指针的大小为8字节。

因此总大小为16字节。
再看上面的题:
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() {
A a;
B* p = new B;
p->test();
p->func();
return 0;
}

3.2多态的原理
多态的原理也就是虚函数表里函数指针指向谁调用谁。
虚函数表:
-
基类对象的虚函数表中存放基类所有的虚函数指针。
-
每个包含虚函数的类(或其派生类)会有一个虚函数表,这是一个存储函数指针的数组,记录了该类所有虚函数的地址。
-
同类对象共享一个虚函数表。

-
基类和派生类各自维护自己的虚函数表。如果派生类重写了基类的虚函数,派生类的虚函数表中会替换为自身的函数地址;未重写的虚函数则沿用基类的地址。
-
没有重写的普通派生类,和基类的虚函数表按理上是一样的,但是不是同一个表。

-
**虚函数存在哪里?**虚函数和普通函数一样,编译好是一段指令,都是存在代码段,虚表中存放的是虚函数指针。
-
**虚函数表存在哪里?**具体没有规定,和编译器有关。
3.3动态绑定和静态绑定
-
对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
-
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
1355

被折叠的 条评论
为什么被折叠?



