参考: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)
(2 条消息) C++为什么要弄出虚表这个东西? - 知乎 (zhihu.com)
C++基础-动态多态的理解 - 知乎 (zhihu.com)
(虚继承)防止重复内容的有趣操作 - 知乎 (zhihu.com)
c++多态和虚函数表实现原理 - 知乎 (zhihu.com)
(2 条消息) 多态实现原理——虚函数表原理解析,干货满满,面向对象特性 - 知乎 (zhihu.com)
深入剖析C++多重继承的虚函数表 - 知乎 (zhihu.com)