静态绑定#
C++ 中函数的默认绑定是静态绑定,也称为早绑定和编译期绑定。
静态绑定将函数的查找、关联的过程放在编译期间完成,可以提升程序的运行时的性能。
动态绑定#
#include <iostream>
class Base {
public:
void foo() {
std::cout << "Base::foo" << std::endl;
}
};
class Derive1 : public Base {
public:
void foo() {
std::cout << "Derive1::foo" << std::endl;
}
};
int main() {
Base *base = new Derive1();
base->foo();
delete base;
return 0;
}
运行结果:
Base::foo
这里并没有调用派生类的函数,因为编译器在这里默认进行静态绑定,编译阶段编译器无法确定 Base 类型指针会指向什么类型,无法根据对象指针实际指向的对象类型来进行函数调用。
所以需要将函数的绑定从编译阶段推迟到运行阶段,从而使对象指针能够指向实际指向的类型。这就是动态绑定,也称晚绑定和运行时绑定。
将基类不进行静态绑定的成员函数声明为 virtual
虚函数,从而实现动态绑定。
#include <iostream>
class Base {
public:
virtual void foo() {
std::cout << "Base::foo" << std::endl;
}
virtual ~Base() {}
};
class Derive1 : public Base {
public:
void foo() {
std::cout << "Derive1::foo" << std::endl;
}
};
int main() {
Base *base = new Derive1();
base->foo();
delete base;
return 0;
}
运行结果:
Derive1::foo
Derive2::foo
虚函数表#
虚函数的动态绑定基于虚函数表(vftable)实现,当类中存在虚函数时,编译器会在对象的第一个数据成员位置插入 vfptr 指针,指向一个包含所有虚函数地址的数组。
编译器会为每个包含虚函数的类生成虚函数表,该类的所有对象共享一个虚函数表,观察下面代码:
#include <iostream>
class Base {
public:
virtual void foo() {}
};
int main() {
Base b1;
void **b1_vfptr = *(void ***)&b1;
Base b2;
void **b2_vfptr = *(void ***)&b2;
std::cout << (b1_vfptr == b2_vfptr) << std::endl;
return 0;
}
运行结果:
1
虚函数表包含该类及基类中所有虚函数的地址,观察下面代码:
#include <iostream>
class Base {
public:
virtual void foo() {
std::cout << "Base::foo" << std::endl;
}
virtual ~Base() {}
};
class Derive : public Base {
public:
void foo() override {
std::cout << "Derive::foo" << std::endl;
}
virtual void foo2() {
std::cout << "Derive::foo2" << std::endl;
}
};
int main() {
Base *base = new Derive;
void **vfptr = *(void ***)base;
// [0] &Base::foo
// [1] &Base::~Base
// [2] &Derive::foo
// [3] &Derive::foo2
void (*f)() = (void(*)())vfptr[3];
f();
delete base;
return 0;
}
运行结果:
Derive::foo2
虚函数表由编译器负责初始化和销毁,在构造函数中初始化,在析构函数中销毁。
在多重继承下,可能会有多个虚函数表。
#include <iostream>
class Base1 {
public:
virtual void foo() {}
};
class Base2 {
public:
virtual void foo() {}
};
class Derive : public Base1, public Base2 {
public:
void foo() override {}
};
int main() {
std::cout << sizeof(Derive) << std::endl;
return 0;
}
64 bit OS 运行结果:
16
当子类没有重写父类虚函数时,子类的虚函数表会继承父类的虚函数地址。
当子类重写了父类的虚函数,子类从父类继承的虚函数表就会产生对应的覆盖行为。
#include <iostream>
class Base {
public:
virtual void foo() {
std::cout << "Base::foo" << std::endl;
}
virtual ~Base() {}
};
class Derive1 : public Base {
public:
};
class Derive2 : public Base {
public:
virtual void foo() override {
std::cout << "Derive2::foo" << std::endl;
}
};
int main() {
Base *base = new Derive1();
base->foo();
delete base;
base = new Derive2();
base->foo();
delete base;
return 0;
}
运行结果:
Base::foo
Derive2::foo
运行时类型识别#
C++ 是静态类型语言,数据类型在编译阶段确定,但在某些场景下(多态),编译阶段无法确定数据类型,需要在运行阶段才能确定。
运行时类型识别(RTTI)是在运行阶段确定数据类型的机制。
typeid#
typeid
运算符用于获取变量类型,可以在编译阶段获取变量类型,也可以在运行阶段获取变量类型。
运行阶段确定变量类型:
class Base {
public:
virtual ~Base() {}
};
class Derive : public Base {
public:
};
int main() {
Base *base = new Derive;
std::cout << typeid(*base).name() << std::endl;
delete base;
return 0;
}
g++ 运行结果:
6Derive
dynamic_cast#
dynamic_cast
用于类型转换,能够检测具有继承关系的父子类型的指针、引用的类型转换是否安全,可以在编译阶段转换,也可以在运行阶段转换。
编译阶段转换:
#include <iostream>
class Base {};
class Derive : public Base {};
int main() {
Derive *derive = new Derive();
Base *base = dynamic_cast<Base *>(derive);
delete base;
return 0;
}
将一个较大的寻址范围的指针转换为较小的范围,不会导致内存越界,所以是安全的。
运行阶段转换:
#include <iostream>
class Base {
public:
virtual ~Base() {}
};
class Derive : public Base {};
int main() {
Base *base1 = new Base(); // base1 指向基类
Derive *derive1 = dynamic_cast<Derive *>(base1); // 基类转换为派生类
std::cout << derive1 << std::endl; // 0 转换失败
Base *base2 = new Derive(); // base2 指向派生类
Derive *derive2 = dynamic_cast<Derive *>(base2); // 派生类转换为基类
std::cout << derive2 << std::endl; // 非 0 转换成功
delete derive1;
delete derive2;
return 0;
}
将一个较小的寻址范围的指针转换为较大的范围,可能会导致内存越界,所以是不安全的。
typeid
、dynamic_cast
在运行阶段进行类型识别时,依赖于虚函数机制。
在多态的情况下,可能会出现父类指针指向子类对象的情况。
虚函数表中的每一个指针通常指向 std::type_info
对象,可以通过 vfptr 访问到对象的类型信息 &class_meta
。
虚函数机制的优缺点#
优点:
- 动态多态性: 允许通过基类指针或引用调用派生类中重写的虚函数。
- 代码重用: 基类定义公共接口,派生类可以根据需要重写虚函数,实现不同的功能。
- 可扩展性: 可以添加新的派生类,并重写虚函数,而无需修改基类代码。
- 解耦: 虚函数机制可以帮助解耦代码,使代码更易于维护和修改。
缺点:
- 内存开销:每包含虚函数的类都有一个虚函数表,虚函数表是一个函数指针数组,在 64 位操作系统中消耗 8 字节内存。
- 调用开销:调用虚函数时,会通过虚函数表来查找实际需要调用函数的地址,与直接通过函数地址调用函数相比略慢。
- 可能影响优化:静态绑定可以在编译阶段确定调用函数的地址,编译器可以进行内联优化。而对于动态绑定,编译器无法在编译阶段确定具体的函数实现,只能在运行阶段通过虚函数表访问实际调用函数的地址。