什么是绑定?
绑定就是对函数调用和函数定义的映射。
静态绑定(static binding)§
静态绑定发生在编译期,因此也被称为early binding。编译器(或链接器)直接关联内存地址和函数调用。
对普通的函数调用默认采用静态绑定,函数重载(function overloading)和操作符重载(operator overloading)也是如此。
类的类型必须在编译期确定,然后才能进行绑定。static
、private
、final
函数的绑定都发生在编译期。
动态绑定(dynamic binding)§
动态绑定发生在运行期,也被称为late binding。动态绑定基于对象的类型。
在C++中使用虚函数和函数重写(function overriding)时,将会进行动态绑定。
当使用基类指针调用虚函数时,绑定关系将在运行期确定。如果指针指向派生类对象,运行时将执行派生类中的对应函数。
比较§
静态绑定 | 动态绑定 | |
---|---|---|
发生时间 | 编译期 | 运行期 |
也称作 | early binding | late binding |
多态 | 编译期多态 | 运行期多态 |
何时 | 当调用函数所需的所有信息在编译期可用时 | 当编译器不能确定解析函数调用的所有信息时 |
举例 | 普通函数调用/函数重载/操作符重载 | 虚函数调用 |
性能 | 更快 | 慢一些 |
灵活性 | 调用关系在编译期确定 | 通过一个函数调用可以使用不同类型对象的不同函数 |
示例情况§
静态函数§
静态函数不能被重写,静态函数必须在编译期完成静态绑定。
当在派生类中定义一个与基类中的静态函数具有相同签名的静态函数时,可以正确编译,但无法触发多态机制,说明静态函数不能在运行期绑定。
/*
A
B
A
A
*/
#include <iostream>
using namespace std;
class A {
public:
static void hello() {
cout << "A" << endl;
}
};
class B : public A {
public:
static void hello() {
cout << "B" << endl;
}
};
int main() {
A a;
a.hello();
B b;
b.hello();
A *a1 = new A;
a1->hello();
A *a2 = new B;
a2->hello();
return 0;
}
空指针对象调用成员函数§
在工作中意外发现空指针也可以调用成员函数,这也是记录本篇文章的原因。
在以下的示例代码中,类A包含4个public成员函数:普通函数/静态函数/虚函数/final
说明符修饰的虚函数。
根据运行结果,即使类A的指针a为空,仍可以调用一些成员函数。仅有func3()
为虚函数,在运行时引起了段错误。不过,使用final
修饰的虚函数func4()
同样可以正确调用。
原因正是,普通函数和静态函数调用为静态绑定,即使不存在对象实例,也可以进行调用。虚函数为动态绑定,因此空指针调用虚函数会引起运行时错误。final
的作用应该是将虚函数限定为静态绑定,既有利于代码的可读性和可维护性,也有利于性能改善。
C++出于存储和运行效率的考虑,类成员的存储分两部分,一部分由单个实例所有,如非静态成员变量;一部分由所有实例共享,如静态成员。从空间局部性上看,这样的布局有利于Cache命中。
/*
func1
func2
func4
*/
class A {
public:
void func1() {
cout << "func1" << endl;
}
static void func2() {
cout << "func2" << endl;
}
virtual void func3() {
cout << "func3" << endl;
}
virtual void func4() final {
cout << "func4" << endl;
}
};
int main() {
A *a = nullptr;
a->func1();
a->func2();
// a->func3();
a->func4();
return 0;
}