复习:虚函数、纯虚函数;虚继承、虚基类

虚函数与虚继承是2个完全不同的概念。

一、为什么要有虚函数?

  • 虚函数用来实现C++多态机制。
  • 多态的实现效果:多态实现了同样的调用语句具有不同的表现形态
  • 多态的实现要素:虚函数、父类指针指向子类对象、相同的调用代码。

假设有一个函数,它在不同的子类中实现各不相同,那么就将其声明为虚函数,并在子类继承基类后,再对该虚函数改写。

/* 父类 */
class parent {
public:
	void virtual fun();
};
void parent::fun() {
	printf("base\n");
}
/* 子类 */
class child :public parent {
public:
	void virtual fun();
};
void child::fun() {
	printf("child\n");
}
/* 使用父类指针指向子类对象 */
int main(int argc, char* argv[])
{
	parent* c = new( child );
	/* 该虚函数对应的类是child */
	c->fun();
	return 0;
}

使用父类指针指向子类对象,调用结果是:child
意味着,可以任意增加child这样的子类,然后使用父类指针指向子类对象,并使用相同的调用代码,但基于不同的子类可以完成不同的功能,即多态。

	parent* c = new( child );
	c->fun();
	return 0;

1.1、虚函数指针

若在类的定义中出现了关键字virtual,那么该类就会创建一个(该类私有的)虚函数表,该表存在与内存的某个位置(由虚函数指针确定),该虚函数表中存放了虚函数指针。

因此,若该类中出现了1或多个virtual函数,那么相应的sizeof就要加上4个字节(虚函数指针的大小)。

1.2、什么是纯虚函数

纯虚函数是一种特殊的虚函数,它的一般格式如下(C++格式):看到=0就是纯虚函数

class <类名>
{
virtual <类型><函数名>(<参数表>)=0;};
  • 为什么要有纯虚函数?

纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

  • 纯虚函数的特征:
    一般而言纯虚函数的函数体是缺省的,但是也可以给出纯虚函数的函数体(此时纯虚函数仍然为纯虚函数,对应的类仍然为抽象类,还是不能实例化对象)调用纯虚函数的方法为:抽象类类名::纯虚函数名(实参表)
    函数声明:virtual <类型><函数名>(<参数表>)=0;

二、为什么要有虚继承?

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题。

图解

2.1、虚基类指针

虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(存在与内存中的某个位置,由虚基类指针确定)
(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了)
当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

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

2.2、虚基类与虚函数

在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。

虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。

虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

2.3、菱形结构的虚继承中,构造函数的是如何调用的?

  • 在派生类的构造函数的成员初始化列表中,必须列出虚基类构造函数的调用。如果没有列出,则表示使用该虚基类的缺省构造函数。

  • 在虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中,都要列出对虚基类构造函数的调用。但只有用于建立对象的当前层派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类构造函数的调用在执行中被忽略,从而保证对虚基类构造函数只调用一次

  • 在一个成员初始化列表中,同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行

示例程序:观察类的构造函数的调用顺序以及次数。

class A
{
public:
    A(const char*s)
    {
        cout<<s<<endl;
    }
};
class B:virtual public A
{
public:
    B(const char*s1,const char*s2):A(s1)
    {
        cout <<s2<<endl;
    }
};

class C:virtual public A
{
public:
    C(const char*s1,const char*s2):A(s1)
    {
        cout<<s2<<endl;
    }
};

class D:public B,C
{
public:
    D(const char *s1,const char *s2,const char*s3,const char*s4):B(s1,s2),C(s1,s3),A(s1)
    {
        cout <<s4<<endl;
    }
};
int main(int argc, char* argv[])
{
    D *ptr = new D("class A","class B","class C","class D");
    delete ptr;
    ptr = NULL;
    return 0;
}
输出:
class A
class B
class C
class D

三、虚继承与普通继承的sizeof的对比

  • class A
    1个虚函数,则需要1个虚函数表指针
  • class B
    继承方式是虚继承,则子类需要一个虚基类表指针
    子类没有新的虚函数,则继承父类的虚函数表指针
  • class C
    继承方式是虚继承,则子类需要一个虚基类表指针
    子类定义了新的虚函数,由于继承方式是虚继承,则不与父类共享一个虚函数表,又增加了一个虚函数表指针,存在2个虚函数表指针
  • class D
    继承方式是普通共有继承,则没有虚基类表指针
    子类定义了新的虚函数,与父类共享一个虚函数表指针
class A {
public:
	virtual void f() {
		cout << "A" << endl;
	}
};

class B :virtual public A {
	virtual void f() {
		cout << "B" << endl;
	}
};

class C :virtual public A {
public:
	void f() {
		cout << "C" << endl;
	}
	virtual void q() {}

};
class D :public A {
public:
	void f() {
		cout << "D" << endl;
	}
	virtual void W() {}
};

int main(){
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;
	cout << sizeof(D) << endl;
	return 0;
}

上例的运行结果是:

4:虚函数表指针
8:虚基类表指针+虚函数表指针
12:虚基类表指针+虚函数表指针+虚函数表指针
4:虚函数表指针

使用的VS2015,有人运行结果不一样,可能是编译器不一样。VS编译器跟g++编译器就不是同一个。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值