浅谈c++函数调用以及析构函数为虚函数的原因
文章目录
- 前言
- 一、成员函数调用
- 1.普通成员函数调用
- 2.虚成员函数的调用方式
- 二、静态绑定与动态绑定
- 1.概念
- 三、基类析构函数为虚函数原因
- 总结
前言
巩固c++知识点,从编译器的角度看看程序的函数调用。
一、成员函数调用
1.普通成员函数调用
示例
class base
{
public:/*virtual void myfun1(){printf("base::myfun1()\n");}virtual void myfun2(int value = 1){printf("base::myfun2()=%d\n",value);}*/base(){printf("base::base()\n");}void myfun3(){m_i = 5;printf("base::myfun3()\n");}~base(){printf("base::~base()\n");}int m_i;
};int main()
{base base;base.myfun3();return 0;
}
反汇编查看函数调用:
将base对象放入寄存器中,再通过函数地址调用函数。由此看出,成员函数,和普通的函数其实并没有太多的区别。只是因为函数内部,要访问类成员变量,所以通过寄存器将自身的地址传入到函数中,这样在函数中就能访问成员变量。
2.虚成员函数的调用方式
示例:
class base1
{
public:virtual void myfun1(){printf("base1::myfun1()\n");}virtual void myfun2(int value = 1){printf("base1::myfun2()=%d\n",value);}void myfun3(){printf("base::myfun3()\n");}int m_i1 = 5;
};class base2 :public base1
{
public:base2(){printf("test\n");}virtual void myfun1(){printf("base2::myfun1()\n");}virtual void myfun2(int value = 2){printf("base::myfun2()=%d\n", value);}void myfun3(){printf("base2::myfun3()\n");}virtual ~base2(){printf("~base2::base2()\n");}int m_i3 = 6;
};
当通过子对象调用,直接调用虚函数时:
base2 base2;
base2.myfun1();
通过以上反汇编的结果,可以知道,通过定义的对象访问虚函数时,编译器直接通过函数地址来进行调用的。
当通过类指针访问虚函数时:
base2 *pbase2 = new base2();pbase2->myfun1();base1 *pbase1 = new base2();pbase1->myfun1();
当子类指针指向子类对象时,调用虚函数反汇编如下:
因为是子类指针,所以子类对象首地址会放入寄存器中。获取虚函数表,放入rax中。最后通过虚函数表找到函数地址,进行调用。
关于虚函数的调用,可参考:https://blog.csdn.net/qq_39884728/article/details/143563901
当父类指针指向子类时,查看反汇编:
pbase1就是子类对象,通过观察也是通过虚函数表指针调用的。
二、静态绑定与动态绑定
1.概念
当一个子类和一个父类都有一个相同名字的成员函数时,此时调用那个函数,根据静态绑定相关。
示例:
base2 *pbase2 = new base2();pbase2->myfun3();base1 *pbase1 = new base2();pbase1->myfun3();
反汇编查看pbase2->myfun3() 如下:
因为是子类指针,所以此时调用子类的成员函数。
反汇编查看pbase1->myfun3() 如下:
因为是父类指针,所以此时调用父类的成员函数。
虚函数的动态绑定:
base2 *pbase2 = new base2();pbase2->myfun1();base1 *pbase1 = new base2();pbase1->myfun1();
输出结果:
因为都是子类的对象,同时子类重写了虚函数,更改了虚函数表,不和父类的一样。所以调用的是子类的方法。
缺省参数是静态绑定的:
示例:
base2 *pbase2 = new base2();pbase2->myfun2();base1 *pbase1 = new base2();pbase1->myfun2();
输出结果:
通过结果可知,缺省参数的输入,和静态绑定的指针类型有关。
三、基类析构函数为虚函数原因
示例:
class base1
{
public:virtual void myfun1(){printf("base1::myfun1()\n");}virtual void myfun2(int value = 1){printf("base1::myfun2()=%d\n",value);}void myfun3(){printf("base::myfun3()\n");}virtual ~base1(){printf("~base1::~base1()\n");}int m_i1 = 5;
};class base2 :public base1
{
public:base2(){printf("test\n");}virtual void myfun1(){printf("base2::myfun1()\n");}virtual void myfun2(int value = 2){printf("base::myfun2()=%d\n", value);}void myfun3(){printf("base2::myfun3()\n");}virtual ~base2(){printf("~base2::base2()\n");}int m_i3 = 6;
};
int main()
{base1 *pbase2 = new base2();delete pbase2;return 0;
}
反汇编查看base2的析构函数如下:
在base2的析构函数结束后,会调用base1的析构函数。
观察父类析构函数的反汇编:
可以看到父类的析构函数,只存在对自己的操作。
当父类的析构函数不是虚函数时,那么根据静态绑定的定义。此时删除指针时,不会调用虚函数表,而是直接调用父类的析构函数。导致子类的析构函数没被调用。
由此推断,当对象是子类对象时,只要调用到子类的析构函数,(结束时会调用父类的析构函数)对象就能完全释放。
那么根据静态绑定的原则,当子类指针,指向子类对象时。删除指针,此时,会调用子类的析构函数。
示例:
将上面的虚析构函数,改为析构函数
base2 *pbase2 = new base2();delete pbase2;
结果:
总结
简单的总结了一下函数调用,文章关于虚函数的内容,并没有进行详细的解释。如有好奇的读者,可参考:
链接: 浅谈c++虚函数
相关博客:
浅谈c++虚基类内存布局