ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

c++笔记之虚函数、虚继承、多态和虚函数表

2021-10-29 18:34:12  阅读:175  来源: 互联网

标签:函数 int 多态 c++ class Base virtual public


参考:C++——来讲讲虚函数、虚继承、多态和虚函数表 - 知乎 (zhihu.com)

1、什么是虚函数?

虚函数是一种由virtual关键字修饰的一种类内函数,可分为虚函数和纯虚函数。还是先上代码看看吧:

#include<iostream>
#include<memory>

class A
{
public:
    virtual void func(){std::cout<<"A func() called"<<std::endl;}

};

class B:public A
{
public:
    void func(){std::cout<<"B func() called"<<std::endl;}


};



int main()
{

    A a;
    a.func();

    B b;
    b.func();


    return 0;
}

运行结果:

ok,我们来看看virtual关键字在这里的作用,类B继承于类A,但类B中有和A同名的func函数,这个时候声明一个类B的对象,它就能正确地调用B的func。

你这个时候可能会有疑问,virtual关键字我没看到在这起了什么作用啊?

那我们把类A的virtual去掉,再看看输出:

输出为:

没错!这时候发现去掉virtual关键字与否并不改变输出结果。。。看起来virtual在这里没有起到任何作用。

我们等下再说为什么要把需要重写的方法用virtual修饰,现在你就先认为它没用吧!

首先,我们补充一个知识点:析构函数可以写成虚的,但是构造函数不行。

为什么呢?其中的原因比较复杂,简单地来说就是虚函数是通过一种特殊的功能来实现的,它存储在类所在的内存空间中,构造函数一般用于申请内存,那连内存都没有,怎么能找到这种特殊的功能呢?

所以构造函数不能是虚的。当然还有其他原因,具体地原因可以参考以下文章:

为什么构造函数不能为虚函数

好,现在我们来试试把析构函数写成虚的,来看看会发生什么事?

#include<iostream>
#include<memory>

class A
{
public:
    A(){std::cout<<"A() called"<<std::endl;}
    virtual ~A(){std::cout<<"~A() called"<<std::endl;}

};

class B:public A
{
public:
    B(){std::cout<<"B() called"<<std::endl;}
    ~B(){std::cout<<"~B() called"<<std::endl;}


};



int main()
{

    B b;


    return 0;
}

运行结果如下:

好,那么我们来观察一下这里的virtual有什么用呢?你可以尝试把virtual去掉,观察一下输出有没有不同。

结论是,没有不同,无论基类的析构函数virtual与否,输出都是这样的。

啊咧,那给析构函数加虚有什么用啊!那你现在也暂且先认为它没用吧……

那我们来讲一讲纯虚函数。

还是最前面的例子程序,将:

virtual void func(){std::cout<<"A func() called"<<std::endl;}

修改为:

virtual void func()=0;

这样类A的func就是一个纯虚函数。这个时候我们再编译一下,出现以下错误:

#include<iostream>
#include<memory>

class A
{
public:
    virtual void func()=0;
};

class B:public A
{
public:
    void func(){std::cout<<"B func() called"<<std::endl;}

};


int main()
{

    A a;
    a.func();

    B b;
    b.func();

    return 0;
}

变量类型A是一个抽象类(因为凡是包含纯虚函数的类都是抽象类),在抽象类A中不能够被执行的纯虚方法func; 不能为抽象类A声明一个实例对象!!!

也就是等同于如下:

error C2259: “A”: 不能实例化抽象类
note: 由于下列成员:
note: “void A::func(void)”: 是抽象的
note: 参见“A::func”的声明

对!纯虚函数是不能被调用的,因为它根本就没有具体实现,只有声明。所以a.func();这样的代码是会报错的。

那么我们把代码改成以下这样:

#include<iostream>
#include<memory>

class A
{
public:
    virtual void func()=0;

};

class B:public A
{
public:
    void func(){std::cout<<"B func() called"<<std::endl;}


};



int main()
{
    B b;
    b.func();


    return 0;
}

运行结果如下:

注意:

(1)好,那我们就知道了纯虚函数是一种不需要写实现,只需要写声明的一种函数,它留待派生类来实现它的具体细节,我们在这里称A为基类,B为派生类,下文同。

(2) 此外需要额外注意,因为类A拥有纯虚函数。所以我们也称类A为抽象类,称A::func()为抽象函数。

(3)请记住,抽象类是不能被实例化的,也就是说A a;这句语法是非法错误的。

那问题又来了,派生类可以是抽象类吗?

我们不妨试一试:

#include<iostream>
#include<memory>

class A
{
public:
    virtual void func()=0;

};

class B:public A
{
public:
    void func()=0;


};

class C:public B
{
  public:
    void func(){std::cout<<"C func() called"<<std::endl;}
};



int main()
{
    C c;
    c.func();


    return 0;
}

运行结果如下:

从这个结果可以发现:在单继承的前提下,你只要实例化的派生类不是抽象类就可以了,且一个抽象类是可以继承自抽象类的,并且它可以被另一个类所继承。

2、什么是虚继承?

先看代码如下:

#include<iostream>
#include<memory>

class A
{
public:
    int a;

};

class B:public A
{
public:
    int b;


};

class C:public A
{
  public:
    int c;
};

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


int main()
{
    D d;
    d.a = 5;


    return 0;
}

结果:

main.cpp(26): error C2385: 对“a”的访问不明确
main.cpp(26): note: 可能是“a”(位于基“A”中)
main.cpp(26): note: 也可能是“a”(位于基“A”中)
main.cpp(27): error C2385: 对“a”的访问不明确
main.cpp(27): note: 可能是“a”(位于基“A”中)
main.cpp(27): note: 也可能是“a”(位于基“A”中)

后三条和前三条报的都是一样的错误。

指的就是D这个类被实例化了之后,对象d想访问基类A的成员a的时候,竟然不知道应该是通过B来找还是通过C来找。

这种就叫做菱形继承,如图:

那这个时候我们该如何访问到经过B的A的成员a呢?以下代码给出解决方案:

#include<iostream>
#include<memory>

class A
{
public:
    int a;

};

class B:public A
{
public:
    int b;


};

class C:public A
{
  public:
    int c;
};

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


int main()
{
    D d;
    d.B::a = 5;
    std::cout<<d.B::a<<std::endl;

    return 0;
}

运行结果如下:

好的我们终于发现了一种经过指定类访问到爷类(基类的基类)的成员方法了!

那我又提出了一个新问题:经过B访问的a和经过C访问的a,它们,是一个a吗?

我们做个实验就知道了,因此,我们给出如下代码:

#include<iostream>
#include<memory>

class A
{
public:
    int a;

};

class B:public A
{
public:
    int b;


};

class C:public A
{
  public:
    int c;
};

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


int main()
{
    D d;
    std::cout<<&d.B::a<<std::endl;
    std::cout<<&d.C::a<<std::endl;

    return 0;
}

地址都不同,那就肯定不是一个a了,但是它们的地址位置相差8,这难道是一个巧合吗?

我们后面会说到,可以证明它们的偏移量在这个例子中,是不会随着代码执行的次数的多少而有所改变的。

那其实也就是说,如果是这样继承,D中将会有两份A的副本

这不对劲,我们应该只想要一份A而已。这个时候我们就需要引入虚继承了,在需要继承的基类前加virutal关键字修饰该基类,使其成为虚基类,见代码如下:

#include<iostream>

class A
{
public:
	int a;

};

class B :virtual public A
{
public:
	int b;
};

class C :virtual public A
{
public:
	int c;
};

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

int main()
{
	D d;
	std::cout << &d.a << std::endl;
	std::cout << &d.B::a << std::endl;
	std::cout << &d.C::a << std::endl;
	
	return 0;
}

运行结果如下:

我们可以发现,无论指不指定经过的类,a都只会在d中有一份副本了。

原文中给出但要记住并注意:把上面代码中的

class D :public B, public C

全部写成

class D :virtual public B, virtual public C

是不可以实现多继承!!!!!

但我这里发现改完以后也可以正常编译运行,代码如下:

#include<iostream>

class A
{
public:
	int a;

};

class B :virtual public A
{
public:
	int b;
};

class C :virtual public A
{
public:
	int c;
};

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

int main()
{
	D d;
	std::cout << &d.a << std::endl;
	std::cout << &d.B::a << std::endl;
	std::cout << &d.C::a << std::endl;
	
	return 0;
}

3、多态

我们来解决第一节中所提出的问题,在基类中给成员函数/析构函数分别加virtual到底有什么作用?

我们先来看看C++是如何实现多态的,见如下代码,代码给出了一种基类对象调用派生类中的方法的例子:

#include<iostream>


class Base
{
public:

	virtual void func() { std::cout << "Base func() called" << std::endl; }
};

class Derived :public Base
{
public:

	void func() { std::cout << "Derived func() called" << std::endl; }
};

int main()
{

	Base *b = new Derived;
	b->func();
	return 0;
}

可以发现:给Base类的指针赋予派生类的属性,居然可以正确调用派生类中的方法!

那我们把Base类的virtual删掉呢?

那输出就会变成为:

我们可以发现,这个时候派生类中的方法就不会去覆盖基类中的同名方法,从而无法调用派生类的方法。

那么同样地,我么可以猜想,如果Base类的析构函数不虚,将会发生怎么样的结果?

#include<iostream>


class Base
{
public:
	Base() { std::cout << "调用Base类的构造函数" << std::endl; }
	~Base() { std::cout << "调用Base类的析构函数" << std::endl; }
	
};

class Derived :public Base
{
public:
	Derived() { std::cout << "调用Derived类的构造函数" << std::endl; }
	~Derived() { std::cout << "调用Derived类的析构函数" << std::endl; }

	
};

int main()
{

	Base *b = new Derived;
	delete b;
	return 0;
}

我们可以看见结果得出以分析结论:

1)Base *b = new Derived;即调用了Base类的构造函数也调用了Derived类的构造函数,且注意Base *b是在栈内存中申请的Base类指针b,new Derived是在堆内存中申请的Derived类的指针。

2)Base b;这样只会调用Base类的构造函数,没有涉及到Derived类的东西。

3)从结果中发现Derived类的空间没有被析构,即没有被释放,也就是发生了内存泄露(其实这里说泄漏是存在不严谨的,因为整个程序结束之后所有东西都会被系统回收,就没有所谓的内存泄漏一说了

注意:如果把Base类的析构函数设置为虚的,那就有:

#include<iostream>


class Base
{
public:
	Base() { std::cout << "调用Base类的构造函数" << std::endl; }
	virtual ~Base() { std::cout << "调用Base类的析构函数" << std::endl; }
	
};

class Derived :public Base
{
public:
	Derived() { std::cout << "调用Derived类的构造函数" << std::endl; }
	~Derived() { std::cout << "调用Derived类的析构函数" << std::endl; }

	
};

int main()
{

	Base *b = new Derived;
	delete b;
	return 0;
}

这样就可以成功释放所有申请的内存了!!!

好,析构函数的虚特性我们就搞清楚了,但是我们还是没有搞清楚一个问题:

把函数声明为虚的,为什么Base的指针就可以在绑定派生类的属性之后寻找到正确的方法呢?

接下来我就来讲虚函数表及虚函数表指针,这两者是实现多态的必备工具。

同时,我们将会讨论内存分布,并且我们通过虚函数表明白,为什么不能把基类中的方法赋值给派生类指针

4、虚函数表及虚函数表指针

我们先来一段不表现多态的代码,来探究一下虚函数表及其指针的是什么,见如下代码:

#include<iostream>


class Base
{
public:
	Base() { std::cout << "调用Base类的构造函数" << std::endl; }
	virtual ~Base() { std::cout << "调用Base类的析构函数" << std::endl; }
	virtual void func() { std::cout << "调用Base类中的func()函数." << std::endl; }
	
};

class Derived :public Base
{
public:
	Derived() { std::cout << "调用Derived类的构造函数" << std::endl; }
	~Derived() { std::cout << "调用Derived类的析构函数" << std::endl; }
	void func() { std::cout << "调用Derive类中的func()函数" << std::endl; }

	
};

int main()
{

	Base *b = new Base;
	Derived* d = new Derived;
	delete b;
	delete d;
	return 0;
}

这时候我们打开监视,看一下类对象有什么东西:

其中,b的地址为0x0000027ae0af2c30,其中有一个隐藏变量__vfptr,类型为void**,地址为0x00007ff63928bc30。

d的地址为0x00000149f5142570,其中有一个Base类,Base类下有一个隐藏变量__vfptr,类型为void**,地址为0x00007ff63928bcb0。

那么这个__vfptr指向什么呢?,由其类型void**可以知道它应该指向一个void*类型的指针,即void**是指向指针的指针,即二维指针,也可以看作是二维数组。就是我们的虚函数表。我们仅看b,打开b的__vfptr往下展开:

我们看到里面有两个数据,一个([0])是Base的析构函数的地址,地址为0x00007ff639281230;另一个([1])是Base的func,地址为0x00007ff6392814a1。

这个void**其实就存放着所有被virtual关键字修饰的函数的实际存放地址。void*就是指针,其值就是一个地址。

我们可以看看__vfptr指向的变量叫什么名字:project15.exe!void(*Base::`vftable`[3])()

就是一个叫`vftable`的函数指针数组(也即是void**类型)(指针数组:数组每一个元素都是一个指针,参考博客),长度为3。(为什么长度为3呢?)

好,这个虚函数表就真相大白了。

接下来我们就把那个上面不表现多态的代码改一下,使其展现出多态,具体代码如下:

#include<iostream>

class Base
{
public:
	Base() { std::cout << "调用基类Base的构造函数" << std::endl; }
	virtual ~Base() { std::cout << "调用基本Base的析构函数" << std::endl; }
	virtual void func() { std::cout << "调用基类函数func" << std::endl; }
};

class Dervied :public Base
{
public:
	Dervied() { std::cout << "调用派生类Dervied的构造函数" << std::endl; }
	~Dervied() { std::cout << "调用派生类Derivied的析构函数" << std::endl; }
	void func() { std::cout << "调用派生类函数func的析构函数" << std::endl; }

};



int main()
{
	Base* b = new Base();
	Dervied* d = new Dervied();
	delete b;
	delete d;

	b = new Dervied();
	delete b;
	
	return 0;
}

现在我们来看一下指针变量b的状态:

我们可以发现,这个时候的b是一个Base类的指针,但是__vfptr指向的确是Dervied类的虚函数表。此外发现其地址0x00007ff65629bca8,这与上次d(即上面这张图所展示的)的

__vfptr保存的地址值是一样的。

那么这很好,那我要调用函数的时候(比如调用func),我要去虚函数表寻找函数地址的吗?那我不就一找就能够找到我想调用的Dervied类中的方法了?那我们也知道,在整个程序的生存周期中,

每个类的虚函数表都有唯一的一个地址。

那回到之前的问题,为什么不能把基类的属性赋值给派生类的指针呢?我们来举一个例子就知道了,如下代码例子:

#include<iostream>

class Base
{
public:
	Base() {}
	virtual ~Base() {}
	virtual void func() { std::cout << "调用基类函数func" << std::endl; }
	virtual void func2() { std::cout << "调用基类函数func2" << std::endl; }
};

class Dervied :public Base
{
public:
	Dervied() {}
	~Dervied() {}
	void func() { std::cout << "调用派生类func函数" << std::endl; }
	virtual void func3() { std::cout << "调用派生类func3函数" << std::endl; }

};



int main()
{
	Base* b = new Base();
	Dervied* d = new Dervied();
	delete b;
	delete d;
	
	return 0;
}

我们来看看b和d的虚函数表:

可以发现:b的虚函数表的长度为4,而d的虚函数表的长度为5。

那也就是说,Dervied类对象指针本该接收一个长度为5的虚函数表,可是你给他传了一个Base类的属性,整个Base类只有长度为4的虚函数表,没办法填满这个长度为5的虚函数表。

那也就是说,缺失了一种方法的实现,缺失了哪个方法呢?

答案是:缺失了func3()

假设我有语句:

Dervied *d = new Base();

并且假设这个语句合法,那么我们随即调用Derived的func3()方法:

d->func3();

请记住:就算你把Base类的属性赋值给了d,可d本身依然还是一个Derived类的指针,编译器是不会管你把什么属性赋值给了d的。所以d->func3();这个语句本来就应该是合法的。

那这个时候程序跳转到d的虚函数表(注意这个虚函数表是Base类的虚函数表),发现找不到func3()方法,所以就崩了。

 

      由于派生类继承了基类所有的公有虚函数,所以派生类是基类的超集(对应相反的就是子集)。所以把派生类属性赋值给基类是合法的,但基类赋值给派生类就一定是不合法的,因为基类缺失了一些派生类新定义的属性(即在基类中找不到派生类中新定义的成员)。

至于给基类种的private属性的函数打virtual会怎样?

你可以试试,在派生类中根本就没办法重写这些虚方法,也没法访问,一样是没有意义的。

 

基础篇:继承,多态和虚函数 - 知乎 (zhihu.com)

(30条消息) C++多态虚函数表详解(多重继承、多继承情况)_青城山小和尚-CSDN博客_多继承虚函数表

(30条消息) C++(刨根问底)_虚函数_多态_抽象类_多态的原理_dodamce的博客-CSDN博客

C++虚函数表(多态的实现原理) (biancheng.net)

虚函数、虚函数表、虚继承_Fiona_新浪博客 (sina.com.cn)

C++ 为什么不把所有函数设置成虚函数? - 知乎 (zhihu.com)

请问这个c++多继承问题? - 知乎 (zhihu.com)

(1 条消息) c++虚函数的作用是什么? - 知乎 (zhihu.com)

12.虚函数与多态 - 知乎 (zhihu.com)

(2 条消息) C++为什么要弄出虚表这个东西? - 知乎 (zhihu.com)

C++基础-动态多态的理解 - 知乎 (zhihu.com)

(虚继承)防止重复内容的有趣操作 - 知乎 (zhihu.com)

c++多态和虚函数表实现原理 - 知乎 (zhihu.com)

(2 条消息) 多态实现原理——虚函数表原理解析,干货满满,面向对象特性 - 知乎 (zhihu.com)

深入剖析C++多重继承的虚函数表 - 知乎 (zhihu.com)

C++多态 - 知乎 (zhihu.com)

标签:函数,int,多态,c++,class,Base,virtual,public
来源: https://blog.csdn.net/yangjinyi1314/article/details/113858294

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有