代码仓库shanchuann/CPP-Learninng

编译时多态(静态多态)

编译时多态在程序编译阶段完成函数地址绑定,执行效率高,核心实现方式为函数重载、运算符重载,以及模板多态。模板多态也被称为参数化多态,是C++中另一种强大的编译时多态机制,它通过模板参数在编译时生成具体的函数或类实现,从而实现“一份代码,多种类型适配”的效果。

函数重载

同一个类中可以定义多个同名函数,只要参数列表(个数、类型、顺序)不同,编译器会根据实参自动匹配对应版本。这种匹配是在编译阶段完成的,编译器会检查实参与形参的类型兼容性,选择最匹配的函数进行调用,不会产生运行时额外开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Calculator {
public:
// 重载1:两个整数相加
int add(int a, int b) { return a + b; }
// 重载2:三个整数相加
int add(int a, int b, int c) { return a + b + c; }
// 重载3:两个浮点数相加
double add(double a, double b) { return a + b; }
};
int main() {
Calculator calc;
cout << "两个整数相加:" << calc.add(10, 20) << endl;
cout << "三个整数相加:" << calc.add(10, 20, 30) << endl;
cout << "两个浮点数相加:" << calc.add(1.5, 2.5) << endl;
return 0;
}

在这个例子中,编译器会根据main函数中传入的实参类型和数量,自动选择对应的add函数版本。比如传入两个整数时,调用 add(int a, int b); 传入两个浮点数时,调用 add(double a, double b);。整个过程在编译时就已经确定,运行时直接跳转到对应函数地址执行,效率很高。

运算符重载

为自定义类型重新定义运算符行为,本质是函数重载,关键字为operator。运算符重载可以让自定义类型的对象像内置类型一样使用运算符,大大提升代码的可读性和直观性。不过运算符重载不能改变运算符的优先级、结合性和操作数个数,也不能创建新的运算符,只能重载已有的运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 运算符:实现复数相加
Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); }
// 重载 << 运算符:实现复数的输出,需要声明为友元函数以访问私有成员
friend ostream& operator<<(ostream& os, const Complex& c);
void show() const { cout << real << " + " << imag << "i" << endl; }
};
// 友元函数实现 << 运算符重载
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
int main() {
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3 = c1 + c2; // 调用重载的 +
cout << "复数相加结果:" << c3 << endl; // 调用重载的 <<
return 0;
}

这里不仅重载了加法运算符,还重载了输出流运算符 <<。输出流运算符通常需要声明为类的友元函数,因为它的第一个操作数是 ostream 对象,而不是类的对象,无法作为类的成员函数实现。通过友元函数,我们可以直接访问类的私有成员real和imag,完成复数的输出。

模板多态(参数化多态)

模板多态是编译时多态的另一种重要形式,包括函数模板和类模板。模板本身不是具体的函数或类,而是一个“蓝图”,编译器会根据传入的模板参数在编译时生成具体的函数或类实例,这个过程称为模板实例化。模板多态实现了代码的泛化,让一份代码可以适配多种数据类型,同时保持编译时绑定的高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 函数模板:通用加法
template <typename T>
T add(T a, T b) {
return a + b;
}
// 类模板:通用数组
template <typename T, int size>
class Array {
private:
T arr[size];
public:
void set(int index, T value) {
if (index >= 0 && index < size) arr[index] = value;
}
T get(int index) const { return arr[index]; }
};
int main() {
// 函数模板实例化
cout << "整数相加:" << add<int>(10, 20) << endl;
cout << "浮点数相加:" << add<double>(1.5, 2.5) << endl;
// 也可以让编译器自动推导模板参数类型
cout << "自动推导类型相加:" << add(30, 40) << endl;
// 类模板实例化
Array<int, 5> intArr;
intArr.set(0, 100);
cout << "整数数组第一个元素:" << intArr.get(0) << endl;
Array<double, 3> doubleArr;
doubleArr.set(1, 3.14);
cout << "浮点数数组第二个元素:" << doubleArr.get(1) << endl;
return 0;
}

函数模板add可以适配任何支持+运算符的类型,比如int、double,甚至是自定义的Complex类(只要Complex类重载了+运算符)。类模板Array可以创建任意类型、任意大小的数组,避免了为每种类型单独写一个数组类的重复工作。编译器在编译时会根据模板参数生成具体的函数和类,比如add、Array<int,5>等,这些生成的代码和普通的非模板代码效率完全相同。

运行时多态(动态多态)

运行时多态是面向对象的核心,依赖继承和虚函数实现,在程序运行阶段根据对象实际类型动态绑定函数地址。运行时多态必须同时满足三个条件:存在继承关系,基类中至少有一个虚函数(virtual修饰),通过基类指针或引用指向子类对象,调用虚函数。除了这些基础条件,运行时多态还有很多重要的细节和扩展知识点,比如协变返回类型、虚函数默认参数、纯虚函数实现、RTTI等。

基础示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 基类:图形
class Shape {
public:
// 虚函数:绘制图形
virtual void draw() const {
cout << "绘制一个通用图形" << endl;
}
// 虚析构函数:防止内存泄漏
virtual ~Shape() {}
};
// 派生类:圆形
class Circle : public Shape {
public:
void draw() const override { // C++11 override 显式标记重写
cout << "绘制一个圆形" << endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override {
cout << "绘制一个矩形" << endl;
}
};
// 通用绘制函数:通过基类指针调用
void drawShape(const Shape* shape) {
shape->draw(); // 动态绑定:根据实际对象类型调用对应版本
}
int main() {
Shape* s1 = new Circle();
Shape* s2 = new Rectangle();
drawShape(s1); // 输出:绘制一个圆形
drawShape(s2); // 输出:绘制一个矩形
delete s1;
delete s2;
return 0;
}

在这个例子中,drawShape函数接收的是Shape*指针,但传入的是Circle和Rectangle对象。当调用 shape->draw() 时,程序不会根据指针的声明类型Shape来调用函数,而是根据指针指向的实际对象类型(Circle或Rectangle)来调用对应的draw函数,这就是动态绑定。C++11引入的override关键字可以显式标记子类重写了基类的虚函数,如果子类的函数签名和基类不一致,编译器会直接报错,避免了拼写错误或参数不匹配导致的意外创建新函数。

虚析构函数的必要性

如果基类析构函数不是虚函数,通过基类指针删除子类对象时,只会调用基类的析构函数,导致子类资源泄漏。这是因为析构函数的调用如果不是虚函数,就会采用静态绑定,只根据指针的声明类型来调用析构函数。而如果基类析构函数是虚函数,删除时会先调用子类的析构函数,再调用基类的析构函数,确保子类和基类的资源都能正确释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 错误示例:基类析构函数非虚
class BaseWrong {
public:
~BaseWrong() {
cout << "BaseWrong 析构函数" << endl;
}
};
class DerivedWrong : public BaseWrong {
private:
int* data;
public:
DerivedWrong() {
data = new int[10];
cout << "DerivedWrong 构造函数" << endl;
}
~DerivedWrong() {
delete[] data;
cout << "DerivedWrong 析构函数(释放资源)" << endl;
}
};
// 正确示例:基类析构函数为虚
class BaseRight {
public:
virtual ~BaseRight() { cout << "BaseRight 析构函数" << endl; }
};
class DerivedRight : public BaseRight {
private:
int* data;
public:
DerivedRight() {
data = new int[10];
cout << "DerivedRight 构造函数" << endl;
}
~DerivedRight() {
delete[] data;
cout << "DerivedRight 析构函数(释放资源)" << endl;
}
};
int main() {
cout << "错误示例" << endl;
BaseWrong* w = new DerivedWrong();
delete w; // 只调用 BaseWrong 析构,DerivedWrong 资源泄漏
cout << "\n正确示例" << endl;
BaseRight* r = new DerivedRight();
delete r; // 先调用 DerivedRight 析构,再调用 BaseRight 析构,资源正确释放
return 0;
}

在错误示例中,delete w时只调用了BaseWrong的析构函数,DerivedWrong中分配的data数组没有被释放,造成内存泄漏。而在正确示例中,delete r时先调用DerivedRight的析构函数释放data数组,再调用BaseRight的析构函数,资源完全正确释放。因此,只要一个类可能被继承,它的析构函数就应该声明为虚函数,这是C++面向对象编程的一个重要规范。

纯虚函数与抽象类

纯虚函数是在基类中声明为virtual 函数名 = 0的虚函数,包含纯虚函数的类称为抽象类,抽象类无法实例化对象,必须由子类重写纯虚函数后才能实例化。纯虚函数的存在强制子类实现特定接口,是面向接口编程的基础。不过需要注意的是,纯虚函数也可以在类外有实现,子类可以通过作用域解析符调用基类的纯虚函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 抽象类:动物
class Animal {
public:
// 纯虚函数:发出声音
virtual void makeSound() const = 0;
// 普通成员函数
void eat() const { cout << "动物在吃东西" << endl; }
virtual ~Animal() {}
};
// 纯虚函数可以在类外有实现(虽然不常见,但语法允许)
void Animal::makeSound() const {
cout << "动物发出通用的声音" << endl;
}
// 派生类:狗
class Dog : public Animal {
public:
void makeSound() const override { cout << "汪汪汪" << endl; }
// 可以调用基类的纯虚函数实现
void makeBaseSound() const { Animal::makeSound(); }
};
// 派生类:猫
class Cat : public Animal {
public:
void makeSound() const override { cout << "喵喵喵" << endl; }
};
int main() {
// Animal a; // 错误:抽象类无法实例化
Animal* d = new Dog();
Animal* c = new Cat();
d->makeSound(); // 输出:汪汪汪
c->makeSound(); // 输出:喵喵喵
d->eat(); // 输出:动物在吃东西
// 调用基类的纯虚函数实现
static_cast<Dog*>(d)->makeBaseSound(); // 输出:动物发出通用的声音
delete d;
delete c;
return 0;
}

在这个例子中,Animal类是抽象类,无法创建对象,但它的纯虚函数makeSound可以在类外有实现。Dog类不仅重写了makeSound,还提供了makeBaseSound函数来调用基类的纯虚函数实现。这种设计虽然不常见,但在某些场景下很有用,比如基类提供一个默认的实现,子类可以选择直接使用或者在此基础上扩展。

协变返回类型

协变返回类型是C++中虚函数重写的一个特殊规则,它允许子类重写虚函数时,返回值类型是基类虚函数返回值类型的子类(前提是返回值是指针或引用)。协变返回类型的存在让多态更加自然,子类可以返回更具体的类型,而不需要强制类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 基类:产品
class Product {
public:
virtual void show() const { cout << "这是一个通用产品" << endl; }
virtual ~Product() {}
};
// 派生类:具体产品A
class ProductA : public Product {
public:
void show() const override { cout << "这是产品A" << endl; }
void specificFuncA() const { cout << "产品A的特有功能" << endl; }
};
// 基类:工厂
class Factory {
public:
// 虚函数:创建产品,返回Product*
virtual Product* createProduct() const { return new Product(); }
virtual ~Factory() {}
};
// 派生类:具体工厂A
class FactoryA : public Factory {
public:
// 协变返回类型:返回ProductA*,是Product*的子类
ProductA* createProduct() const override { return new ProductA(); }
};
int main() {
Factory* f = new FactoryA();
// 因为协变返回类型,createProduct返回的实际是ProductA*
Product* p = f->createProduct();
p->show(); // 输出:这是产品A
// 可以直接转换为ProductA*,调用特有功能
ProductA* pa = static_cast<ProductA*>(p);
pa->specificFuncA(); // 输出:产品A的特有功能
delete p;
delete f;
return 0;
}

在这个工厂模式的例子中,Factory类的createProduct返回Product*,而FactoryA类重写createProduct时返回ProductA*,这就是协变返回类型。因为ProductA是Product的子类,所以这种重写是合法的。协变返回类型让我们在使用FactoryA时,可以直接得到ProductA*类型的对象,不需要额外的类型转换,代码更加自然和安全。

虚函数的默认参数陷阱

虚函数的默认参数是静态绑定的,而不是动态绑定的。也就是说,即使调用了子类的虚函数,默认参数的值还是会使用基类中定义的值,而不是子类中定义的值。这是一个非常容易出错的地方,需要特别注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
// 虚函数,默认参数为10
virtual void func(int x = 10) const {
cout << "Base::func,x = " << x << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
// 重写虚函数,默认参数改为20(但这个默认参数不会生效!)
void func(int x = 20) const override {
cout << "Derived::func,x = " << x << endl;
}
};
int main() {
Base* b = new Derived();
b->func(); // 调用的是Derived::func,但x的值是Base的默认参数10!
delete b;
return 0;
}

在这个例子中,b指向的是Derived对象,所以调用的是Derived::func,但默认参数x的值却是Base类中定义的10,而不是Derived类中定义的20。这是因为默认参数是在编译时根据指针的声明类型(Base*)来确定的,而不是在运行时根据对象类型确定的。为了避免这种陷阱,最好不要在虚函数中使用默认参数,或者确保子类和基类的默认参数完全一致。

RTTI(运行时类型识别)

RTTI 是 Run-Time Type Identification 的缩写,即运行时类型识别,它允许程序在运行时获取对象的类型信息。C++中主要通过两个机制实现 RTTI:dynamic_cast 和typeid。这两个机制都依赖于虚函数,只有包含虚函数的类才能使用RTTI进行动态类型识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <typeinfo>
class Base {
public:
virtual void func() const { cout << "Base::func" << endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void func() const override {cout << "Derived::func" << endl;}
void derivedFunc() const { cout << "Derived::derivedFunc(特有功能)" << endl; }
};
int main() {
Base* b = new Derived();
// 1. dynamic_cast:安全的向下类型转换
// 尝试将Base*转换为Derived*,如果转换成功,返回有效的Derived*;如果失败,返回nullptr
Derived* d = dynamic_cast<Derived*>(b);
if (d != nullptr) {
cout << "dynamic_cast转换成功" << endl;
d->derivedFunc(); // 可以调用Derived的特有功能
}
else cout << "dynamic_cast转换失败" << endl;
// 2. typeid:获取对象的类型信息
// typeid返回一个type_info对象的引用,可以用==或!=比较类型
cout << "b的类型:" << typeid(*b).name() << endl;
cout << "Derived的类型:" << typeid(Derived).name() << endl;
if (typeid(*b) == typeid(Derived)) cout << "b指向的是Derived对象" << endl;
delete b;
return 0;
}

dynamic_cast 用于将基类的指针或引用安全地转换为子类的指针或引用。如果转换的是指针,失败时返回nullptr;如果转换的是引用,失败时会抛出 bad_cast 异常。typeid用于获取对象的类型信息,它返回一个 type_info 对象,通过 type_info::name() 可以获取类型的名称(不同编译器的名称格式可能不同),也可以直接比较两个 type_info 对象是否相等来判断两个对象的类型是否相同。需要注意的是,只有当类中有虚函数时,typeid才会返回对象的动态类型(实际类型);如果类中没有虚函数,typeid只会返回指针或引用的声明类型。

虚函数表与虚函数指针

每个包含虚函数的类会生成一张虚函数表(vtable),表中存储该类所有虚函数的地址;每个对象会包含一个虚函数指针(vptr),指向所属类的虚函数表。虚函数表和虚函数指针是运行时多态的底层实现基础,编译器通过虚函数指针找到虚函数表,再根据虚函数在表中的位置调用对应的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void nonVirtualFunc() { cout << "Base::nonVirtualFunc" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func3() { cout << "Derived::func3" << endl; }
};
int main() {
Base b;
Derived d;
// 查看对象大小:包含虚函数指针(64位系统占8字节,32位系统占4字节)
cout << "Base 对象大小:" << sizeof(b) << endl;
cout << "Derived 对象大小:" << sizeof(d) << endl;
// 虚函数表验证:通过指针间接调用(仅用于演示,实际开发不建议,因为依赖编译器实现)
using FuncPtr = void (*)();
Base* ptr = &d;
// 虚函数表地址存储在对象的前几个字节(64位是前8字节,32位是前4字节)
long* vptr = (long*)ptr;
long* vtable = (long*)*vptr;
// 调用第一个虚函数:Derived::func1(因为Derived重写了func1)
FuncPtr func = (FuncPtr)vtable[0];
func();
// 调用第二个虚函数:Base::func2(因为Derived没有重写func2)
func = (FuncPtr)vtable[1];
func();
return 0;
}

只有达到同返回类型,同函数名,同参数列表才能构成运行时的多态(注:C++ 允许协变返回类型例外,即重写虚函数时,返回类型可以是基类虚函数返回类型的派生类指针或引用,此情况仍满足多态条件)。编译器在数据区创建一个虚表(Base::vftable),在构建时 Base 要额外多出一个虚表指针 __Vfptr 用于指向 Base 虚表的地址。当开始构建 Derived 时,虚表指针将指向 Derived 的虚表,也就是说虚表最终的指向是最后构建的对象的虚表地址。

image-20260319000947621

在这个例子中,Base 类和 Derived 类的对象大小都包含了虚函数指针的大小(64 位系统是 8 字节)。Base 类的虚函数表中存储了 func1 和 func2 的地址,Derived 类的虚函数表中,func1 的地址被替换为Derived::func1的地址(因为重写了),func2 的地址还是 Base::func2 的地址(因为没有重写),同时 Derived 类自己的 func3 也会被添加到虚函数表中(不过不同编译器的添加位置可能不同)。当通过Base* ptr指向 Derived 对象时,ptr->vptr指向的是 Derived 类的虚函数表,所以调用 func1 时会调用Derived::func1,实现了动态绑定。

当我们定义了 Base 对象并想要其调用 func3 方法时会编译错误,这是因为我们按照类型识别 Base 中并没有 func3 的方法,尽管虚表指针指向的 Derived 具有 func3(补充:这涉及静态类型动态类型的区别 ——Base*是静态类型,决定了成员访问的 “权限范围”;Derived是动态类型,决定了虚函数调用的实际目标。因此即使 Derived 虚表中有 func3,也无法通过 Base 类型访问)。

只有用指针和引用调用虚函数时才查询虚表,用对象名调用时则是静态链接。

总的来说:

  1. 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
  2. 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
  3. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
  4. 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。但内联作为建议,对于编译器来说虚的等级要高于内联,因此同时书写也依然可以编译通过。
  5. 构造函数和拷贝构造函数不能作为虚函数。因为构造函数和拷贝构造函数是用于设置虚表指针的函数。
  6. 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
  7. 实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
  8. 在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。

需要注意的是,静态成员函数、友元函数不能是虚函数,它们不属于对象的成员,无法与具体对象绑定;子类重写虚函数时,函数签名必须与基类完全一致,建议使用override关键字;虚函数会带来轻微的运行时开销(虚函数指针存储、间接寻址),性能敏感场景需权衡。另外,虚函数和虚继承虽然都使用了virtual关键字,但它们的作用完全不同:虚函数是为了实现运行时多态,虚继承是为了解决菱形继承中的数据冗余和二义性问题,不要将两者混淆。

静态联编和动态联编

联编(Binding)是 C++ 中确定函数调用语句与函数执行体对应关系的过程,是函数调用的底层核心环节。根据联编执行的时间节点,分为静态联编(早绑定)动态联编(晚绑定),二者分别对应了 C++ 的编译时多态与运行时多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Base {
public:
// 非虚函数:静态联编
void func() { cout << "Base::func 非虚函数调用" << endl; }
// 虚函数:支持动态联编
virtual void vFunc() { cout << "Base::vFunc 虚函数调用" << endl; }
// 虚析构函数:保证多态场景下完整析构
virtual ~Base() {}
};
class Derived : public Base {
public:
// 同名非虚函数:仅同名覆盖,无多态
void func() { cout << "Derived::func 非虚函数调用" << endl; }
// 重写虚函数:支持动态联编
void vFunc() override { cout << "Derived::vFunc 虚函数调用" << endl; }
};
int main() {
Base b;
Derived d;
// 1. 对象名直接调用:均为静态联编
cout << "对象名直接调用" << endl;
b.func();
b.vFunc();
d.func();
d.vFunc();
// 2. 基类指针指向派生类对象
cout << "\n基类指针调用" << endl;
Base* ptr = &d;
ptr->func(); // 非虚函数:静态联编
ptr->vFunc(); // 虚函数:动态联编
// 3. 基类引用绑定派生类对象
cout << "\n基类引用调用" << endl;
Base& ref = d;
ref.func(); // 非虚函数:静态联编
ref.vFunc(); // 虚函数:动态联编
return 0;
}

静态联编(早绑定)

静态联编指的是在程序编译阶段就完成函数调用与函数体的绑定,编译器根据调用方的静态类型(声明时的类型)直接确定要执行的函数地址,程序运行时不再修改。在上述示例中,所有非虚函数的调用、对象名直接调用的函数,均为静态联编:

  1. 非虚函数 func 的调用,编译器仅看指针 / 引用的静态类型 Base*,直接绑定 Base::func,无论指针实际指向哪个子类对象,都只会执行基类的函数实现;
  2. 用对象名 bd 直接调用函数时,编译器明确知道对象的实际类型,直接绑定对应类的函数,即使是虚函数,也不会触发动态联编。

静态联编的核心优势是执行效率高,编译时直接确定函数地址,无任何运行时额外开销,缺点是不支持运行时多态,灵活性不足。

动态联编(晚绑定)

动态联编指的是在程序运行阶段才完成函数调用与函数体的绑定,编译器在编译时无法确定调用方的实际类型,会在程序运行时,根据调用方的动态类型(实际指向的对象类型),通过虚函数表找到对应的函数执行体,这也是 C++ 运行时多态的底层实现原理。动态联编的触发必须同时满足两个核心条件:

  1. 被调用的函数必须是虚函数,且子类完成了对该虚函数的重写;
  2. 必须通过基类的指针或引用调用该虚函数。

在上述示例中,通过 Base* ptrBase& ref 调用 vFunc() 时,触发了动态联编:编译阶段,编译器仅知道 ptrref 的静态类型是 Base* / Base& ,无法确定实际指向的对象类型;运行阶段,程序通过对象头部的虚函数指针 _vptr 找到对应类的虚函数表,查表获取到重写后的Derived::vFunc地址,执行对应的函数体,最终实现运行时多态。

只有用基类指针和引用调用虚函数时才会触发动态联编、查询虚函数表,用对象名直接调用时,无论是否为虚函数,均为静态联编。

二者差异

  1. 性能差异:静态联编在编译时确定函数地址,执行效率更高;动态联编需要运行时查询虚函数表,有轻微的性能开销,但换来的是极高的代码灵活性和可扩展性。
  2. 适用场景:静态联编适用于函数逻辑固定、无需重写的场景,如工具类函数、非虚成员函数;动态联编适用于需要基于继承实现多态、接口统一但实现差异化的场景,如之前示例中的图形绘制、工厂模式等。
  3. 边界注意:友元函数、全局函数、静态成员函数、构造函数均无法触发动态联编,只有类的非静态虚成员函数,才能通过基类指针 / 引用实现动态联编。