C++多态部分以及虚函数虚表总结

    多态性是面向对象程序设计的一个重要特征,如果一个语言只支持类而不支持多态,是不能被称为面向对象语言的,只能说是基于对象的。

    多态性简单来讲就如同字面意思一般,多种形态,在c++程序设计中,多态性是指具有不同功能函数可以用一个函数,这样就可以用一个函数名调用不同内容的函数。简单来说:向不同的对象发送同一条消息不同的对象在接收时会产生不同的方法。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。

     在类的各类函数中,我们曾研究过的运算符重载其实就是一种多态性的实例。最简单的例如加减乘除,编译器会根据你前后两个数字的类型来自动调用相关的函数来进行运算。这就是一种多态的现象。

     在我们的方法中,类型简单来划分的话分为静态类型和动态类型。静态类型:对象声明时的类型,是在编译时确定的。动态类型:目前所指向的类型,是在运行时确定的。同理,多态也分为静态多态和动态多态。

     一.静态多态:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推 断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
     二.动态多态:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

而我们在使用动态多态的时候,需要动态绑定。这时候我们就必须引入虚函数的概念。对于动态绑定有两个要求:1,必须使用虚函数。2,通过基类类型的引用或者指针调用函数。

     首先我们需要看一下为什么要引用虚函数:首先我们先举一个例子

#include<iostream>
using namespace std;
class Base1 {

public:

            void f() { cout << "Base1::f" << endl; }

            void g() { cout << "Base1::g" << endl; }

            void h() { cout << "Base1::h" << endl; }

 

};
class Derive : public Base1, public Base2, public Base3 {

public:

            void f() { cout << "Derive::f" << endl; }

            void g1() { cout << "Derive::g1" << endl; }
			
};
void f(Base1&b)
{
	b.f();
}

 

typedef void(*Fun)(void);

 

int main()

{

	Derive d;
	Base1 b1;
	f(d);
	f(b1);
	system("pause");
           return 0;

}

打印结果是两个
Base1::f  
这说明我们在调用基类的打印d函数的时候调用的而是d的基类函数,而当我们调用含有基类Base的派生类的Derive的打印d函数的时候,仍然使用的是基类的打印f函数。这是因为在没加上virtual 这个关键字的时候并没有实现动态的多态,因为在基类里面的这个函数不是虚函数,因此你在传入派生类对象作为参数的时候他依然会去调用基类里面的函数,因此会打印两次基类里面的打印语句,而不会执行派生类的打印语句。所以上文我们说要使用动态多态必须使用虚函数的原因。

所以我们对上面的内容进行改进

#include<iostream>
using namespace std;
class Base1 {

public:

            virtual void f() { cout << "Base1::f" << endl; }

            void g1() { cout << "Base1::g" << endl; }

            void h() { cout << "Base1::h" << endl; }

 

};
class Derive : public Base1, public Base2, public Base3 {

public:

            virtual  void f() { cout << "Derive::f" << endl; }

            virtual void g1() { cout << "Derive::g1" << endl; }
			
};
void f(Base1&b)
{
	b.f();
        b.g1();
}

 

typedef void(*Fun)(void);

 

int main()

{

	Derive d;
	Base1 b1;
	f(d);
	f(b1);
        
 system("pause");
           return 0;

}
结果是

加上了virtual关键字后,我们就可以顺利地调用我们派生类的f函数了。同时我们对比也发现了,只有当基类的指定函数加上关键字后,虚拟函数才能形成,而如果在派生类中的函数加上关键字,函数并没有成为虚函数。所以我们可以得出一下结论:

1、要形成重写必须声明基类里面的函数是虚函数

2、在派生类里面的这个函数必须和基类里面的这个函数是函数名相同,参数列表相同,返回值相同(协变除外)

3、派生类里面可嘉可不加virtual这个关键字,但最好加上

既然虚构函数能给我们带来如此的方便,那么我们是不是可以给所以的函数加上关键字,答案当然是否定的。一些函数从本质上来讲就无法成为虚函数。

1、静态成员函数不能作为虚函数:静态成员函数是没有this指针的,它不是特定的指向某一个对象我们可以通过类名就直接可以访问静态成员函数,但是我们直到虚表指针是要存放在对象的前四个字节中的。

2、友元函数也不能作为虚函数:友元函数本不属于类函数,所以不能作为虚函数。

3、构造函数不能作为虚函数:调用构造函数是为了创建对象,而如果构造函数作为虚函数那么对象还没有创建成功是不可能将它的虚表指针(也就是一个地址)存放在对象的前四个字节中。


同理,有不能成为虚函数的函数,也一定有适用于虚函数的函数。例如赋值运算符作为虚函数,当我们需要调用赋值运算符的时候,我们需要针对参数的不同类型来调用不同的赋值运算符,此时我们就极其需要使其成为虚函数来满足我们的需求。

#include<iostream>
using namespace std;
class Base1 {

public:

            virtual void f() { cout << "Base1::f" << endl; }

            void g1() { cout << "Base1::g" << endl; }

            void h() { cout << "Base1::h" << endl; }
			~Base1()
			{
				cout<<"base 1"<<endl;
			}
				
 

};
class Derive : public Base1//, public Base2, public Base3 {
{
public:

            virtual  void f() { cout << "Derive::f" << endl; }

            virtual void g1() { cout << "Derive::g1" << endl; }
			~Derive()
			{
				cout<<"Derive"<<endl;
			}
};
int main()

{
	Base1 *p=new Derive;
	delete p;
 system("pause");
           return 0;

}
首先我们先不将析构函数定义为虚函数,在这里我们这样定义一个对象。我们定义一个基类指针指向派生类。很明显,创建后他会调用两个类的构造函数进行构造,接下来我们释放这个空间,按照逻辑,我们也应该调用两次析构函数,来删除其中的相关数据。但是在我们不使用虚函数的情况下。实际上的调用情况是。

这。。就很尴尬了。问题出在哪里了呢。。这时我们给基类的析构函数加上关键字(我就不贴代码了)。结果是

这回我们就发现正常了。原因其实也很简单就跟我们头一次举得那个调用例子是相同的。虽然这样定义对象的情况很特殊,但是我们很难保证其在程序设计时不会出现,而如果没有使用虚函数,可想而知,有一部分数据并未被删除,这样就会产生一个问题————内存泄漏。为了避免出现这个问题,使用虚函数就在所难免了。

    虚函数中还有一类比较特殊的就是纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
例如:virtual void funtion1()=0

但是,需要注意的一点是,生命了纯虚数的类是一个抽象类,在使用时就无法用起定义对象,只能将它继承到派生类中去使用。

    虚函数的使用让我们不需要使用指向基类的指针只指向它的基类,而是根据不同的派生类对象,调用其相应的函数。为了能够更好地理解虚函数的意义,我们接下来需要分析一下虚函数的实现。

     首先我们采用类似虚拟继承的探究方法来探究一下使用虚函数的前后,对象的大小是否发生了变化。我们直接套用上面的例子。结果是有虚函数的对象时比没有的多了四个字节。根据我的上一篇虚拟继承的总结进行大胆推测,这四个字节也许也是一个指针指向一片空间。我们打开内存看一下。


果然,在我们存储值的上面有一个地址。这个套路让我们想起了虚拟继承,然而这两个之间还是有差异的,这个地址并不是我们虚拟集成的额偏移量。打开这个地址

再次出现两行地址。然而这并不是俄罗斯套娃(大雾)。我们一般把这个列表叫做虚表,它用来保存虚函数的地址,结合我们的代码来看,我们的代码中就是两个虚函数,所以在此出现了两行地址。

为了验证我们的猜想,又因为我们的虚函数中有一段为了验证而存在的打印文字,我们只需要一个简单的循环来读取虚表中的两个地址,即实现了虚函数的调用,便可以证明这里保存的确实是虚函数的地址,不多说放代码。()

#include<iostream>
using namespace std;
class Base1 {

public:

            virtual void f() { cout << "Base1::f" << endl; }
			int a;
            virtual void g1() { cout << "Base1::g" << endl; }

            void h() { cout << "Base1::h" << endl; }
 

};
typedef void(*Fun)(void);
int main()

{
    Base1 b1;
    b1.a=1;
    Fun *pFun = (Fun*)(*((int*)&b1));  
    while (*pFun)  
    {  
        (*pFun)();  
        pFun = (Fun*)((int*)pFun++);  
    } 

        
 system("pause");
           return 0;

}
    我们在这里定义一个函数指针让我们能够顺利地访问到类中的函数,以此来打印虚表。这段代码的运行结果是


   结果告诉我们这两处地址的确是基类中的两个虚函数。但是虚函数为什么需要这样操作呢?这又回到了我们最初的话题上,这块我们查看的只是基类的虚函数,当我们加入派生类的时候,我们再来看看虚函数到底改变了什么。例行放代码

#include<iostream>
using namespace std;
class Base1 {

public:

            virtual void f() { cout << "Base1::f" << endl; }
			int a;
            virtual void g1() { cout << "Base1::g" << endl; }

            void h() { cout << "Base1::h" << endl; }
 

};
class Derive : public Base1//, public Base2, public Base3 {
{
public:

             virtual void f() { cout << "Derive::f" << endl; }
             virtual void g() { cout << "Derive::h" << endl; }
			 int b;
};

typedef void(*Fun)(void);
int main()

{
	Base1 b1;
	b1.a=1;
	Derive D;
	D.b=2;
	Fun *pFun = (Fun*)(*((int*)&D));  
    while (*pFun)  
    {  
        (*pFun)();  
        pFun = (Fun*)((int*)pFun++);  
    } 

        
 system("pause");
           return 0;

}

运行结果

我们发现这里打印的虚函数表就变成了派生类的虚函数表,这样在我们用外部函数调用时,调用就不在是基类的函数,而是派生类的函数。在这里,编译器对于有关键字的函数进行了不同的处理我们来用内存了解一下。


    这就是我们对象D的一个实现过程了,函数Fun在用指针调用对象的函数的时候,会从虚表中调用它所需要的函数。但是我们会发现一个问题最后的h是基类的 这是为什么呢,这是因为我们的派生类中没有同名的函数,所以虚表中基类的虚函数并未被派生类中的虚函数重写.所以基类的h函数就被保存在虚表中,那么有人会问了,我们能否在派生类中增加虚函数呢?答案是可以的,增加的虚函数会被添加到虚表中最后一个虚函数的后面。

多继承

   我们上面所举得例子全部都是单向继承的派生类中虚函数的情况如果是多继承呢?

#include<iostream>
using namespace std;
class Base1 {

public:

            virtual void f() { cout << "Base1::f" << endl; }
			int a;
            virtual void g1() { cout << "Base1::g" << endl; }
            virtual void h() { cout << "Base1::h" << endl; }
 

};
class Base2 {

public:

            virtual void f() { cout << "Base2::f" << endl; }
			int a;
            virtual void g1() { cout << "Base2::g" << endl; }
            virtual void h() { cout << "Base2::h" << endl; }
 

};
class Derive : public Base1,public Base2
{
public:

             void f() { cout << "Derive::f" << endl; }
			 void g1() { cout << "Derive::g" << endl; }
             //void h() { cout << "Derive::h" << endl; }
			 int b;
};

typedef void(*Fun)(void);
int main()

{
	Base1 b1;
	b1.a=1;
	Derive D;
	D.b=2;
	Fun *pFun = (Fun*)(*((int*)&D));  
    while (*pFun)  
    {  
        (*pFun)();  
        pFun = (Fun*)((int*)pFun++);  
    } 

        
 system("pause");
           return 0;

看到两个地址,大家都懂得。继承了多少个有虚函数的基类,派生类就具备多少个虚表,而如果发生重写,那么派生类中的虚表就会被重写为派生类中的虚函数。以此类推。

菱形继承

最后 我们来看一下菱形继承中使用虚函数和虚拟继承后,内存的情况

#include<iostream>
using namespace std;
class A {

public:

            virtual void f() { cout << "A::f" << endl; }
			int a;
            //virtual void g1() { cout << "A::g" << endl; }
           // virtual void h() { cout << "A::h" << endl; }
 

};
class Base1:virtual public A
{

public:

            virtual void f() { cout << "Base1::f" << endl; }
			int b1;
            virtual void g1() { cout << "Base1::g" << endl; }
            virtual void h() { cout << "Base1::h" << endl; }
 

};
class Base2:virtual public A 
{

public:

            virtual void f() { cout << "Base2::f" << endl; }
			int b2;
            virtual void g1() { cout << "Base2::g" << endl; }
            virtual void h() { cout << "Base2::h" << endl; }
 

};
class Derive : public Base1,public Base2
{
public:

             virtual void f() { cout << "Derive::f" << endl; }
			 virtual void g1() { cout << "Derive::g" << endl; }
              void h() { cout << "Derive::h" << endl; }
			 int b;
};

typedef void(*Fun)(void);
int main()

{
	Base1 b1;
	b1.a=1;
	Derive D;
	D.a=2;

        
 system("pause");
           return 0;

}

我们可以明显地看出在派生类中的内存情况,每一个派生类中有两个指针,一个指向虚表一个指向偏移量各司其职,互不干扰,同时解决了二义性和访问的问题。

总结:

总而言之,言而总之,虚函数在我们C++学习中是不可缺少的一部分,因为他是动态多态的一个必要前提,而我个人认为,纯虚函数的引入,是出于方便访问以及区分派生类与基类函数引用的相关问题而出现的必不可少的一个内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值