【C++】多态与虚函数

时间:2023-03-06 20:03:51


目录

  • ​​虚函数​​
  • ​​多态​​
  • ​​多态的表现形式一​​
  • ​​多态的表现形式二​​
  • ​​多态的作用​​
  • ​​虚函数的访问权限​​
  • ​​多态的实现原理(虚函数表)​​
  • ​​代码​​
  • ​​为什么需要虚析构函数?​​
  • ​​推荐​​
  • ​​纯虚函数和抽象类​​

虚函数

virtual只能存在于类定义的函数声明中,定义函数的时候不用

【C++】多态与虚函数

多态

多态的表现形式一

派生类的指针可以赋值给基类指针。

通过基类指针调用基类和派生类中的同名虚函数时,如果指针指向的是基类对象,则调用基类的虚函数;反之,调用派生类的虚函数

【C++】多态与虚函数

多态的表现形式二

派生类的对象可以赋值给基类引用

通过基类引用调用基类和派生类中的同名虚函数时,若该引用指向的是一个基类对象,那么调用基类虚函数;反之,调用派生类的虚函数。

【C++】多态与虚函数

多态的作用

增强了程序的可扩充性。

在构造函数和析构函数中调用虚函数,不是多态,。在编译时就可以确定。调用的函数是自己的类或者基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。我们不建议这样做,因为:1. 构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别。2.即使构造函数或者析构函数如果能成功调用虚函数, 程序的运行结果也是不可控的

【C++】多态与虚函数

在非构造、非虚构函数的成员函数中调用虚函数,是多态

【C++】多态与虚函数

关于上述两种情况的例子

【C++】多态与虚函数

一点解释:第一次在构造函数中调用虚函数因为son中有hello这个虚函数,所以直接调用自己的;最后的析构函数中,先执行groundson的析构函数,再执行son的析构函数,因为son本身不含虚函数,所以调用基类的虚函数,也就是“bye from myclass”。


【C++】多态与虚函数


派生类和基类中同名的虚函数,不用加virtual关键字

虚函数的访问权限

【C++】多态与虚函数

多态的实现原理(虚函数表)

【C++】多态与虚函数

【C++】多态与虚函数

每一个有虚函数的类(或有虚函数的派生类)都有一个虚函数表,实例化一个对象,该对象的第一个存储单元存放的必定是虚函数表的指针p_table,这个指针指向虚函数表table,而虚函数表table中存放的是对应的各种虚函数的地址。当一个类调用通名虚函数时,就去虚函数表中找对应类的虚函数。

【C++】多态与虚函数

【C++】多态与虚函数

代码

更换类指针指向的东西(全部,虚函数表+类成员)

注意:其中的拷贝赋值函数“* p2=* p1”,是一个深拷贝,因为经过测试,指向的完全是两块内存。

关于深拷贝、浅拷贝,参考​​我的这篇博客​​

/**
* 测试替换虚函数表以后的类
* */

#include <iostream>

using namespace std;

class A{
public:
A(){b=44;}

virtual void Func(){
cout<<"A::Func"<<endl;
}


int b;

void plus()
{
b+=1;
}

}; // class A

class B:public A
{
private:

public:
B();
~B();

virtual void Func(){
cout<<"B::Func"<<endl;
}

virtual void func(){
cout<<"B::func"<<endl;
}
};

B::B(/* args */)
{
b=55;
}

B::~B()
{
}

int main()
{
A a;
A* pa=new B();
cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;
pa->Func();

// 使用long long类型的指针对象,因为long long对象正好占8个字节
long long* p1=(long long*)&a; // 指针对象,存放虚函数表的地址
long long* p2=(long long*)pa;
cout<<"\nobject address:\n";
cout<<"&a: "<<&a<<", "<<"pa: "<<pa<<endl;
cout<<"p1: "<<p1<<", "<<"p2: "<<p2<<endl;
// cout<<"sizeof(address): "<<sizeof(p2)<<endl;

cout<<"\nVtable address:\n";
cout<<"*p1 Vtable: "<<*p1<<", "<<"*p2 Vtable: "<<*p2<<endl; // 虚函数表的地址
cout<<"\nAfter changed Vtable address:\n";
*p2=*p1; // 将基类的虚函数表的地址赋值给派生类的虚函数表地址
cout<<"*p1 Vtable: "<<*p1<<", "<<"*p2 Vtable: "<<*p2<<endl; // 虚函数表的地址

pa->Func();

// cout<<sizeof(A)<<","<<sizeof(B)<<endl;
// cout<<sizeof(*pa)<<endl;
cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;

a.plus();
cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;

pa->plus();
cout<<"a.b: "<<a.b<<", pa->b: "<<pa->b<<endl;

return 0;

}
/*
a.b: 44, pa->b: 55
B::Func

object address:
&a: 0x7ffff0f827b0, pa: 0x7fffe95f3e70
p1: 0x7ffff0f827b0, p2: 0x7fffe95f3e70

Vtable address:
*p1 Vtable: 140703602580792, *p2 Vtable: 140703602580760

After changed Vtable address:
*p1 Vtable: 140703602580792, *p2 Vtable: 140703602580792
A::Func
a.b: 44, pa->b: 55
a.b: 45, pa->b: 55

*/

为什么需要虚析构函数?

参考:​​C++中虚析构函数的作用及其原理分析​​

因为在删除派生类对象的时候,我们希望可以和构造类似,先调用派生类的析构,在调用基类的析构。

但是实际上,当删除基类指针指向的额派生类对象时,仅仅调用了基类的析构函数数。
这样会造成内存泄露。

#include <iostream>
using namespace std;


class son
{
private:
/* data */
public:
son(/* args */);
~son();
};

son::son(/* args */)
{
}

son::~son()
{
cout<<"~son"<<endl;

}

class groundson : public son
{
private:
/* data */
public:
groundson(/* args */);
~groundson();
};

groundson::groundson(/* args */)
{
}

groundson::~groundson()
{
cout<<"~goundson"<<endl;
}

int main(int argc, char const *argv[])
{
son* p = new groundson();
delete p;
return 0;
}

/*
输出:
~son
仅调用了基类的析构函树
*/

如何解决?

答:将基类的析构函数定义为虚函数。

#include <iostream>
using namespace std;


class son
{
private:
/* data */
public:
son(/* args */);
virtual ~son();
};

son::son(/* args */)
{
cout<<"son"<<endl;
}

son::~son()
{
cout<<"~son"<<endl;

}

class groundson : public son
{
private:
/* data */
public:
groundson(/* args */);
~groundson();
};

groundson::groundson(/* args */)
{
cout<<"groundson"<<endl;
}

groundson::~groundson()
{
cout<<"~goundson"<<endl;
}

int main(int argc, char const *argv[])
{
son* p = new groundson();
delete p;
return 0;
}

/*
输出:
son
groundson
~goundson
~son
*/

推荐

  1. 作为基类使用的类,将析构函数设置为虚函数。
    解释一下:当然我们为了保险起见,所有的类的析构函数都设置成虚函数是最好的,但是虚函数是借助虚函数表作用的,这会占据内存空间。因此,只有当一个类作为基类,才将析构函数设置为虚函数.
  2. 一个类中有虚函数,那么将其析构函数设置为虚函数。
    解释:有虚函数,就表明这个类将来会成为父类,同上。

纯虚函数和抽象类

【C++】多态与虚函数

【C++】多态与虚函数

纯虚函数:没有函数体的虚函数

抽象类:包含纯虚函数的类

抽象类不能创建对象,只能作为基类指针或基类引用指向派生类对象。

在抽象类的成员函数内部可以调用纯虚函数(多态),但是在构造函数或析构函数内部不能调用纯虚函数>(因为非多态,在构造函数或者析构函数内部调用同名的虚函数,其结果,一定是当前类的虚函数,但是当前类的虚函数又是纯虚函数,连函数体都没有,所以不能调用)。

如果一个类中从抽象类派生而来,那么只有当他实现了基类中所有的纯虚函数,他才能称为非抽象类。

【C++】多态与虚函数

纯虚函数:没有函数体的虚函数

抽象类:包含纯虚函数的类

抽象类不能创建对象,只能作为基类指针或基类引用指向派生类对象。

在抽象类的成员函数内部可以调用纯虚函数(多态),但是在构造函数或析构函数内部不能调用纯虚函数(因为非多态,在构造函数或者析构函数内部调用同名的虚函数,其结果,一定是当前类的虚函数,但是当前类的虚函数又是纯虚函数,连函数体都没有,所以不能调用)。

如果一个类中从抽象类派生而来,那么只有当他实现了基类中所有的纯虚函数,他才能称为非抽象类。

【C++】多态与虚函数