C++虚函数,抽象类

——虚方法

虚方法就是允许被其子类重新定义的方法,在声明时,需要使用virtual修饰符。

注意:

1virtual修饰符不能与staticabstract或者override修饰符同时使用;

2)由于虚方法不能是私有的,所以,virtual修饰符不能与private修饰符同时使用。


  使用virtual修饰符声明一个虚方法,用来计算两个数的和
public virtual int Add(int x, int y)                          //定义一个虚方法
{
    return x + y;                                            //返回两个数的和
}



——有关虚函数的疑问


为什么构造函数不能定义为虚函数?

关于C++为什么不支持虚拟构造函数,Bjarne很早以前就在C++Style and Technique FAQ里面做过回答
Avirtual call is a mechanism to get work done given partialinformation. In particular, "virtual" allows us to call afunction knowing only an interfaces and not the exact type of theobject. To create an object you need complete information. Inparticular, you need to know the exact type of what you want tocreate. Consequently, a "call to a constructor" cannot bevirtual.
含义大概是这样的:虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。

从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。

网络上还有一个很普遍的解释是这样的:虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

本人对这个观点并不认同,这主要是因为用什么方式实现虚函数是编译器的事情,使用Vtable只是大多数编译器采用的一种手段,并不代表编译器实现不了虚构造函数,编译器之所以不支持虚构造函数主要原因就是没有必要,所以正好这种实现方式也不支持,巧合而已。


为什么析构函数应当是虚函数
如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。

情况1:用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构

情况2:用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。


为什么友元函数不能是虚函数?
友元函数不是类成员,而只有类成员才能是虚函数。
而且C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

















——重写方法

如果一个方法声明中含有override修饰符,则称该方法为重写方法,它主要用来使用相同的签名重写继承的虚方法。
虚方法主要用来引入新方法,而重写方法则使从基类继承而来的虚方法专用化(提供虚方法的具体实现)。

注意:

1override修饰符不能与newstatic或者virtual修饰符同时使用,另外,重写方法只能用于重写基类中的虚方法,不能用来单独声明方法;

2)重载和重写是不相同的,重载是指编写一个与已有方法同名,但参数列表不同的方法,而重写是指在派生类中重写基类的虚方法。


  创建一个控制台应用程序,首先定义一个基类,并在其中定义一个虚方法,用来计算两个数的和;然后使Program类继承于BaseClass类,并在该类中重写基类中的虚方法,使其实现计算3个数的和;最后在Main方法中,使用派生类对象实例化基类的一个对象,并使用该基类对象调用派生类中的方法,实现计算3个数的和。代码如下:
class BaseClass                                                          //定义一个基类
{
    public virtual int Add(int x, int y)                                 //定义一个虚方法
    {
        return x + y;                                                    //返回两个数的和
    }
}
class Program:BaseClass                                                   //定义一个派生类,继承于BaseClass
{
    static int z = 0;                                                      //定义一个静态变量,用来作为第3个被加数
    public override int Add(int x, int y)                                 //重写基类中的虚方法
    {
        return base.Add(x, y) + z;                                       //计算3个数的和
    }
    static void Main(string[] args)
    {
        z = 698;                                                      //为静态变量赋值
        BaseClass baseclass = new Program();                       //使用派生类对象实例化基类对象
        Console.WriteLine(baseclass.Add(98, 368));            //调用派生类中重写之后的方法
        Console.ReadLine();
    }
}

说明:

Main方法中使用基类对象调用的Add方法是在派生中重写之后的方法,这主要是因为虚方法的实现由派生类中的重写方法进行了取代。

技巧:

在派生类中重写基类中的虚方法时,可以使用base关键字调用基类中的虚方法。




——C++中虚函数的作用和多态

虚函数: 实现类的多态性

关键字:虚函数;虚函数的作用;多态性;多态公有继承;动态联编

C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,😮子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。

当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = &b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual方法,请看后面★*),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。
★如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。

下面的例子解释动态联编性:
class A{
    private:
        int i;
    public:
        A();
        A(int num) :i(num) {};
        virtual void fun1();
        virtual void fun2();

    };

    class B : public A{
    private:
        int j;
    public:
        B(int num) :j(num){};
        virtual void fun2();// 重写了基类的方法
    };

    // 为方便解释思想,省略很多代码

    A a(1);
    B b(2);
    A *a1_ptr = &a;
    A *a2_ptr = &b;

    // 当派生类“重写”了基类的虚方法,调用该方法时
    // 程序根据 指针或引用 指向的  “对象的类型”来选择使用哪个方法
    a1_ptr->fun2();// call A::fun2();
    a2_ptr->fun2();// call B::fun1();
    // 否则
    // 程序根据“指针或引用的类型”来选择使用哪个方法
    a1_ptr->fun1();// call A::fun1();
    a2_ptr->fun1();// call A::fun1();


——虚函数的底层实现机制

实现原理:虚函数表+虚表指针

关键字:虚函数底层实现机制;虚函数表;虚表指针

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

下面的图片体现了上述的底层实现机制:



~C++primer第六版第十三章的虚函数的工作原理
编译器处理虚函数的方法是:
给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了为类对象进行声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中,注意虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。

调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。

使用虚函数后的变化:
(1)    对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2)    每个类编译器都创建一个虚函数地址表
(3)    对每个函数调用都需要增加在表中查找地址的操作。

虚函数的注意事项

总结前面的内容
(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
(3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。

  • 构造函数不能为虚函数。
  • 基类的析构函数应该为虚函数。
  • 友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
  • 如果派生类没有重定义函数,则会使用基类版本。
  • 重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。

对象基本布局:

1.c++对象(基类对象)的内存布局是:对象的内存地址(&a)所指向的内存中的前四个字节中存放的是该对象的虚函数表的首地址(前提是该对象有虚函数),接下来的内存中依次存放该对象的数据成员(非静态的数据成员)。
注意:对象的虚函数表中存放的实际上并不是虚函数的入口地址,而是一个跳转指令(jmp)的地址,该跳转指令,转向虚函数的入口,为了叙述方便,我这里作出约定:我们就认为虚函数表中就存放的是虚函数的入口地址。
虚函数的存放顺序与函数的声明顺序是相同的。

2.派生类的对象的内存布局是:前四个字节依然存放虚表指针,虚表中首先存放父类的虚函数地址,注意,由于派生类中也可能有①自己的虚函数,同时派生类也可以②重写父类的虚函数,虚函数表的分布如何:
对于情况一而言,将派生类新增加的虚函数地址依次添加到虚表(虚表中已经有父类的虚函数地址)的后面。
对于情况二而言,如果派生类重写了父类的虚函数,则将重写后的虚函数地址替换掉父类原来的虚函数地址,如果没有重写,则按照父类的虚表顺序存放虚函数地址
接下来的内存中依次存放该对象的父类的数据成员(非静态的数据成员),然后再存放派生类自己的数据成员。(还有内存对齐的问题)



——抽象类


1.纯虚函数
形式:virtual 函数原型=0//  =0表示没有函数体
定义:在定义一个表达抽象概念的基类时,有时无法给出某些函数的具体实现方法,就可以将这些函数声明为纯虚函数。
特点:无具体实现方法。
2.抽象类
定义:声明了纯虚函数的类,都成为抽象类。
主要特点:抽象类只能作为基类来派生新类,不能声明抽象类的对象。(既然都是一个抽象概念了,纯虚函数没有具体实现方法,故不能创造该类的实际的对象)
但是可以声明抽象类的指针变量或引用变量,通过指针或引用,就可以指向并访问派生类对象,进而访问派生类的成员。(体现了多态性)
作用:因为其特点,基类只是用来继承,可以作为一个接口,具体功能在派生类中实现(接口)

#include "stdafx.h"
using namespace std;
class Base1
{
public:
	virtual void display()=0;//不需要再基类中给出函数的函数体
};
 
class Base2:public Base1
{
public:
	virtual void display();
};
void Base2::display()
{
	cout<<"Base2::display()"<<endl;
}
class derived:public Base2
{
public:
	virtual void display();
};
void derived::display()
{
	cout<<"derived::display()"<<endl;
}
void fun(Base1* pt)
{
	pt->display();
}
int main()
{
	/*Base1 a;*/
	Base2 b;
	derived c;
	//fun(&a);
	fun(&b);
	fun(&c);
	system("Pause");
	return 0;
}












































全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务