C++虚函数机制学习笔记
1.C++
函数调用绑定
C++
中,函数调用绑定是指将函数调用和函数定义进行关联的过程,主要分为静态绑定和动态绑定。
1.1 静态绑定
静态绑定是指在编译时就确定函数调用与函数定义之间关联。编译器根据函数调用处上下文信息,如函数名、参数类型等,来确定调用哪个函数。主要适用于如下情况:
- (1) 非虚函数调用:对于类普通成员函数、静态成员函数和全局函数等非虚函数,编译器在编译时就能确定应该调用哪个函数。
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
void Sum(int a, int b) {
std::cout << "The sum is: " << a + b << std::endl;
}
void Sum(float a, float b) {
std::cout << "The sum is: " << a + b << std::endl;
}
class A {
public:
void Print() {
std::cout << "Hello, World." << std::endl;
}
static void Call() {
std::cout << "A Call..." << std::endl;
}
};
int main(int argc, char *argv[]) {
{
int a = 10;
int b = 20;
Sum(a, b); // calls the first version of Sum()
}
{
float a = 10.5;
float b = 20.5;
Sum(a, b); // calls the second version of Sum()
}
{
A a;
a.Print();
a.Call();
A::Call();
}
return 0;
}
在 main
中,对于 Sum
函数的调用,在编译期间就能通过传入参数类型确定具体调用接口。
- (2) 模板函数实例化:模板函数在编译期根据模板参数进行实例化,生成具体函数定义,这也是静态绑定的一种体现。
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
template <typename T, typename U>
auto Sum(T a, U b) -> decltype(a + b) {
return a + b;
}
int main(int argc, char *argv[]) {
{
int a = 10;
int b = 20;
auto c = Sum(a, b);
std::cout << "Sum of " << a << " and " << b << " is " << c << std::endl;
}
{
float a = 10.5;
double b = 20.5;
auto c = Sum(a, b);
std::cout << "Sum of " << a << " and " << b << " is " << c << std::endl;
}
return 0;
}
1.2动态绑定
动态绑定是指在运行时才确定函数调用和函数定义之间的关联。它利用虚函数机制,通过虚表和虚指针实现,主要用于实现多态。编译器在编译时不会确定调用哪个虚函数,而是在运行时通过对象的虚指针找到对应虚表,再根据虚表中的函数指针调用对应虚函数。
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
class Base {
public:
void func1() { // 非虚函数,静态绑定
std::cout << "Base func1" << std::endl;
}
virtual void func2() { // 虚函数,动态绑定
std::cout << "Base func2" << std::endl;
}
};
class Derived : public Base {
public:
void func1() { // 非虚函数,隐藏基类函数
std::cout << "Derived func1" << std::endl;
}
void func2() override { // 虚函数,覆盖基类虚函数
std::cout << "Derived func2" << std::endl;
}
};
int main(int argc, char *argv[]) {
Base b;
Derived d;
Base *pb = &d;
b.func1(); // 输出:Base func1,静态绑定,调用Base类的func1
b.func2(); // 输出:Base func2,静态绑定,调用Base类的func2
d.func1(); // 输出:Derived func1,静态绑定,调用Derived类的func1
d.func2(); // 输出:Derived func2,静态绑定,调用Derived类的func2
pb->func1(); // 输出:Base func1,静态绑定,调用Base类的func1,因为func1是非虚函数
pb->func2(); // 输出:Derived func2,动态绑定,调用Derived类的func2,因为func2是虚函数
return 0;
}
func1
是非虚函数,采用静态绑定,编译器在编译时就确定了调用哪个函数。而 func2
是虚函数,采用动态绑定,通过基类指针 pb
调用 func2
时,会根据 pb
指向的对象的实际类型 Derived
,调用派生类中的 func2
函数。
2.C++
虚函数机制
2.1 内存布局查看工具使用
我们先使用工具查看一下上面这些类的内存布局,不同工具的命令用法:
编译器 | 用法 |
---|---|
g++ (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 |
g++ -fdump-lang-class [filename] |
clang version 10.0.0-4ubuntu1 |
clang -Xclang -fdump-vtable-layouts -c <filename> |
msvc 2019 |
cl /d1 reportSingleClassLayout<classname> <filename> |
2.2 虚函数表和虚函数指针分析
为了方便分析,将一些无关代码注释掉:
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
// #include <iostream>
class Base {
public:
void func1() { // 非虚函数,静态绑定
// std::cout << "Base func1" << std::endl;
}
virtual void func2() { // 虚函数,动态绑定
// std::cout << "Base func2" << std::endl;
}
};
class Derived : public Base {
public:
void func1() { // 非虚函数,隐藏基类函数
// std::cout << "Derived func1" << std::endl;
}
void func2() override { // 虚函数,覆盖基类虚函数
// std::cout << "Derived func2" << std::endl;
}
};
int main(int argc, char *argv[]) {
Base b;
Derived d;
Base *pb = &d;
b.func1(); // 输出:Base func1,静态绑定,调用Base类的func1
b.func2(); // 输出:Base func2,静态绑定,调用Base类的func2
d.func1(); // 输出:Derived func1,静态绑定,调用Derived类的func1
d.func2(); // 输出:Derived func2,静态绑定,调用Derived类的func2
pb->func1(); // 输出:Base func1,静态绑定,调用Base类的func1,因为func1是非虚函数
pb->func2(); // 输出:Derived func2,动态绑定,调用Derived类的func2,因为func2是虚函数
return 0;
}
clang
下使用如下指令,生成对应虚函数表及虚指针信息:
>> clang -Xclang -fdump-vtable-layouts -c main.cc
Vtable for 'Base' (3 entries).
0 | offset_to_top (0)
1 | Base RTTI
-- (Base, 0) vtable address --
2 | void Base::func2()
VTable indices for 'Base' (1 entries).
0 | void Base::func2()
Vtable for 'Derived' (3 entries).
0 | offset_to_top (0)
1 | Derived RTTI
-- (Base, 0) vtable address --
-- (Derived, 0) vtable address --
2 | void Derived::func2()
VTable indices for 'Derived' (1 entries).
0 | void Derived::func2()
msvc
下使用如下指令,生成对应虚函数表及虚指针信息:
>> cl /d1 reportSingleClassLayoutBase main.cc
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.29.30157 版
版权所有(C) Microsoft Corporation。保留所有权利。
main.cc
class _s__RTTIBaseClassDescriptor size(36):
+---
0 | pTypeDescriptor
8 | numContainedBases
12 | _PMD where
24 | attributes
28 | pClassDescriptor
+---
class _s__RTTIBaseClassArray size(1):
+---
0 | arrayOfBaseClassDescriptors
+---
class Base size(8):
+---
0 | {vfptr}
+---
Base::$vftable@:
| &Base_meta
| 0
0 | &Base::func2
Base::func2 this adjustor: 0
Microsoft (R) Incremental Linker Version 14.29.30157.0
Copyright (C) Microsoft Corporation. All rights reserved.
>> cl /d1 reportSingleClassLayoutDerived main.cc
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.29.30157 版
版权所有(C) Microsoft Corporation。保留所有权利。
main.cc
class Derived size(8):
+---
0 | +--- (base class Base)
0 | | {vfptr}
| +---
+---
Derived::$vftable@:
| &Derived_meta
| 0
0 | &Derived::func2
Derived::func2 this adjustor: 0
Microsoft (R) Incremental Linker Version 14.29.30157.0
Copyright (C) Microsoft Corporation. All rights reserved.
gcc
下使用如下指令,生成对应虚函数表及虚指针信息:
>> g++ -fdump-lang-class main.cc
这时,会生成一个 .class
的文件:
Vtable for Base
Base::_ZTV4Base: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::func2
Class Base
size=8 align=8
base size=8 base align=8
Base (0x0x7fa391f4f420) 0 nearly-empty
vptr=((& Base::_ZTV4Base) + 16)
Vtable for Derived
Derived::_ZTV7Derived: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Derived::func2
Class Derived
size=8 align=8
base size=8 base align=8
Derived (0x0x7fa391dfb1a0) 0 nearly-empty
vptr=((& Derived::_ZTV7Derived) + 16)
Base (0x0x7fa391f4f540) 0 nearly-empty
primary-for Derived (0x0x7fa391dfb1a0)
可以看到,对于 Base
类,它的虚指针 vptr
指向了 Base
虚表中存放的虚函数地址 Base::func2
,对于 Derived
类,虚指针指向了 Derived
虚表中存放的虚函数地址 Derived::func2
。
代码中 Base *pb = &d;
,基类指针 pb
的虚指针会指向派生类 Derived
的虚函数表,因此调用过程中 pb->func2();
会调用派生类的 func2
实现。
2.3 虚函数表与 typeinfo
信息
现在我们看一下虚函数表内容:
Vtable for Base
Base::_ZTV4Base: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::func2
- (1)
0 (int (*)(...))0
:通常是一个特殊的值,用于表示这是一个类型信息指针的占位符。在某些编译器实现中,这个位置可能用于存储一些特殊运行时信息,但具体用途可能因编译器而异。 - (2)
8 (int (*)(...))(& _ZTI4Base)
:& _ZTI4Base
是一个指向Base
类的type_info
对象的指针。type_info
对象包含了 类的类型信息,用于支持RTTI
(运行时类型信息)。例如,dynamic_cast
操作会使用这个指针来检查类型转换的合法性。 - (3)
16 (int (*)(...))Base::func2
:Base::func2
是Base
类中一个虚函数。这个条目存储了func2
函数的地址,使得通过基类指针或引用调用func2
时,能正确调用到Base
类实现。
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual void func2() {
std::cout << "Base::func2" << std::endl;
}
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
int main() {
Base b;
std::cout << typeid(b).name() << std::endl; // 使用RTTI
b.func2();
return 0;
}
3.论基类虚析构函数的必要性
3.1 常规使用
正常实例化一个基类对象和一个派生类对象:
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main(int argc, char *argv[]) {
std::cout << "****************" << std::endl;
{
Base a;
}
std::cout << "****************" << std::endl;
{
Derived b;
}
std::cout << "****************" << std::endl;
{
Base *a = new Base();
delete a;
}
std::cout << "****************" << std::endl;
{
Derived *b = new Derived();
delete b;
}
std::cout << "****************" << std::endl;
return 0;
}
输出结果为:
****************
Base constructor
Base destructor
****************
Base constructor
Derived constructor
Derived destructor
Base destructor
****************
Base constructor
Base destructor
****************
Base constructor
Derived constructor
Derived destructor
Base destructor
****************
可以看到,没有任何问题,实例化一个派生类,会先调用基类构造函数,然后调用派生类构造函数,b
生命周期结束后,会先调用派生类析构函数,然后调用基类析构函数。
3.2 向上转换
所谓向上转换就是将一个基类指针指向派生类对象:
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
~Base() {
std::cout << "Base destructor" << std::endl;
}
void print() {
std::cout << "Base print" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
void print() {
std::cout << "Derived print" << std::endl;
}
};
int main(int argc, char *argv[]) {
Base *b = new Derived();
b->print();
delete b;
return 0;
}
运行结果:
Base constructor
Derived constructor
Base print
Base destructor
可以看到构造过程中,new Derived()
会调用 Derived
类的构造函数,这时会先调用基类构造函数,然后调用派生类的构造内容,但是析构时,指针 b
类型为 Base
,delete
时只会调用 Base
类的析构函数,所以只是析构了基类对象资源,这样就会造成子类对象资源未释放,出现资源泄漏问题。
为什么会这样呢?其实很好理解,因为 b
的指针类型为 Base
,我们在 delete b
时,当然只会调用 Base
的析构函数,同样的,print()
方法调用也只会调用基类的。
3.3 基类虚析构函数
这时,我们只需要将基类的析构函数及 print
函数声明为虚函数,就实现我们预期的结果了:
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
virtual void print() {
std::cout << "Base print" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
void print() {
std::cout << "Derived print" << std::endl;
}
};
int main(int argc, char *argv[]) {
Base *b = new Derived();
b->print();
delete b;
return 0;
}
输出结果:
Base constructor
Derived constructor
Derived print
Derived destructor
Base destructor
print
结果符合预期的原理前面虚函数机制里面已经说明清楚了,那么为什么基类析构函数声明为虚函数,资源释放就能符合预期呢?还是和前面一样,我们先将一些无关代码注释掉:
/*
* @Description: main
* @Author: chenjingyu
* @Date: 2025-01-15 16:21:41
* @FilePath: main.cc
*/
// #include <iostream>
class Base {
public:
Base() {
// std::cout << "Base constructor" << std::endl;
}
virtual ~Base() {
// std::cout << "Base destructor" << std::endl;
}
virtual void print() {
// std::cout << "Base print" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
// std::cout << "Derived constructor" << std::endl;
}
~Derived() {
// std::cout << "Derived destructor" << std::endl;
}
void print() {
// std::cout << "Derived print" << std::endl;
}
};
int main(int argc, char *argv[]) {
Base *b = new Derived();
b->print();
delete b;
return 0;
}
然后使用命令来获取对象布局:
>> g++ -fdump-lang-class main.cc
得到 .class
的输出结果如下:
Vtable for Base
Base::_ZTV4Base: 5 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::~Base
24 (int (*)(...))Base::~Base
32 (int (*)(...))Base::print
Class Base
size=8 align=8
base size=8 base align=8
Base (0x0x7ff89ae07420) 0 nearly-empty
vptr=((& Base::_ZTV4Base) + 16)
Vtable for Derived
Derived::_ZTV7Derived: 5 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Derived::~Derived
24 (int (*)(...))Derived::~Derived
32 (int (*)(...))Derived::print
Class Derived
size=8 align=8
base size=8 base align=8
Derived (0x0x7ff89acb31a0) 0 nearly-empty
vptr=((& Derived::_ZTV7Derived) + 16)
Base (0x0x7ff89ae07840) 0 nearly-empty
primary-for Derived (0x0x7ff89acb31a0)
前面我们知道当 Base *b = new Derived();
时,b
是一个 Base
类型的指针,但是它的虚指针会指向 Derived
类的虚函数表,析构时,先调用虚指针指向的析构函数,然后再调用自身析构函数,这样就实现了正确的资源释放结果。
这里解释一下虚函数表中,两个相同条目:
16 (int (*)(...))Base::~Base
:普通析构函数指针。当通过基类指针删除单个对象时,调用这个指针。24 (int (*)(...))Base::~Base
:删除数组对象的析构函数指针。当通过基类指针删除数组对象时,调用这个指针。
所以,如果基类指针或引用可能用于删除派生类对象,那么基类的析构函数必须声明为虚函数。这样可以确保通过基类指针或引用删除派生类对象时,能够正确调用派生类的析构函数,从而避免资源泄漏。