C++关键字之virtual

C++关键字之virtual

1. 虚基类

C++之继承的多重继承引起的二义性问题中讲到,解决二义性问题的另一个方法就是用关键字virtual。具体做法就是虚基类。

1.1 声明虚基类

虚基类的声明并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。声明时只需在派生类指定继承方式前面加上关键字virtual即可。

class A
{int a};

class B: virtual public A
{int b};

class C:virtual public A
{int c};

class D: public B, public C
{int d};

经过这样的声明后,当基类通过一条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,该基类成员之保留一次。
在派生类B和派生类C做了虚基类声明后,派生类D又把类C和类D作为基类,此时D类中的成员如图所示:
这里写图片描述
可以看到,此时D类中也有A的成员。虽然D类同时继承了B类和C类,但D类中只有“一份”A类的成员。

1.2 虚基类的初始化

如果在虚基类中定义了带参数的构造函数,而且没有定义默认的构造函数,则在其所有派生类(包括直接派生和间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。
在间接派生类中不仅要负责对齐直接基类进行初始化,还要负责对虚基类初始化。

class A
{
    A(int i)
    {}
};

class B: virtual public A
{
    B(int n):A(n)
    {}
};

class C:virtual public A
{
    C(int n):A(n)
    {}
};

class D: public B, public C
{
    D(int n)
    :A(n),
    B(n),
    C(n)
    {}
};
1.3 虚基类的原理

每当派生类声明一个虚基类时,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

2. 虚函数

C++中虚函数是用来解决动态多态问题的。

多态性:向不同对象发送相同的消息,不同的对象在接收时会产生不同的动作。

所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中正式定义此函数。

虚函数的作用是允许在派生类中重定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

2.1 虚函数的使用方法
  1. 在基类中使用virtual声明成员函数为虚函数。在该类外定义虚函数时,不必再加virtual。
  2. 在派生类中重定义此函数时,函数原型要与基类中的函数原型一致。所谓原型一致指定是:函数的返回值、函数名、参数列表都要一样。(这里有两个例外:虚析构函数和协变)
    如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
  3. 定义一个指针向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
  4. 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

如果想调用同一类族中的不同类的同名函数,只要先用基类指针指向该类对象即可。
协变:基类虚函数必须返回基类的指针或引用,派生类虚函数返回派生类对象的指针或引用。
只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数。
一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual但与该虚函数具有相同参数表的和函数返回值类型的同名函数。

2.2 虚构函数

如果将基类的析构函数声明为徐析构函数时,由该基类派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数名和基类的析构函数名不同。

当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类的对象,系统都会自动动态关联并调用相关的析构函数,对该对象进行清理工作。

构造函数不能声明为虚函数。这是因为在执行构造函数时类的对象还未建立,此时便无法完成关联工作。同样拷贝构造函数也不能。
友元函数不能声明为虚函数。这是因为友元函数它本身并不是类的成员函数。
静态成员函数不能声明为虚函数。 这是因为静态成员函数是一个类的成员所共享的,而虚函数是为了给派生类使用的。二者存在的意义不同。

2.3 纯虚函数

当基类中的本身并不需要一个函数,但考虑到派生类的需要,还是在类中预留了一个以函数名,而具体的函数功能交给派生类去定义。

在基类中声明纯虚函数的方法为:只给出函数原型,并在后面加上“=0”。声明纯虚函数的一般形式为:

virtual 函数类型 函数名(参数列表) = 0;

纯虚函数没有函数体
最后面的“=0”并不表示函数的返回值为0,它只起形式上的作用,告诉编译系统这是一个纯虚函数。在VS2013下,这个“=0”是必须要的,否则会编译器会报错
这是一个声明,所以所以最后要有分号

由纯虚函数的一般形式可以看出,纯虚函数的作用只是在基类中为其派生类保留一个函数名,以便派生类根据需要对它进行定义。它本身只有函数名而不具备函数的功能,不能被调用。如果在一个类中声明的纯虚函数,而在其派生类中没有对该函数进行定义,那么该虚函数在派生类中仍是纯虚函数。

2.4 虚函数的原理

在定义了虚函数的类中,C++用到了虚函数表来对其进行管理:

    类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。

    当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。

    如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值