C++ 由虚基类 虚继承 虚函数 到 虚函数表
//虚基类:一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
class Base1{
public:
Base1(){cout<<"Construct Base1!"<<endl;};
void foo();//普通函数
virtual void foo1(){cout<<"foo1 in Base1"<<endl;};//虚函数:可以在基类中实现(+{})或者直接定义成虚基类,\
会出现错误:undefined reference to vtable for lass
virtual void foo2() = 0;//纯虚函数:必须要在继承类中实现,类似java中的接口好处是 可以在不清楚具体构建的情况下,给派生类规定好规范
};
class Base2{
public:
void foo();//普通函数
virtual void foo1();//虚函数
//virtual void foo2() = 0;//纯虚函数
};
//虚继承:继承类继承了基类多次,从而产生了多个拷贝,虚基类的基本原则是在内存中只有基类成员的一份拷贝。\
//这样,通过把基类继承声明为虚拟的,就只能继承基类的一份拷贝,从而消除歧义。
class Derived1: public virtual Base1{
public:
Derived1(){cout<<"Construct Derived1~ !"<<endl;};
void foo2(){
cout<<"foo2 in Derive1"<<endl;
}
};
//多继承
class Derived2: public virtual Base1, public virtual Base2{
void foo2(){
cout<<"foo2 in Derive1"<<endl;
}
};
错误解决:
在使用虚函数的程序中,编译时会出现
undefined reference to `vtable for Class
或 undefined reference to typeinfo for Class 的情况
其解决方案就是将类似于
virtual void foo();
Should be defined (inline or in a linked source file):
virtual void foo() {}
Or declared pure virtual:
virtual void foo() = 0;
基类指针指向派生类
基类指针可以指向派生类,但是无法使用不存在于基类只存在于派生类的元素。(所以我们需要虚函数和纯虚函数)
当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。
于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。
派生类指针不能指向基类
虚函数表
本文部分内容参考了 陈皓专栏的内容.
对c++了解的人都应该知道虚函数是通过一张虚函数表实现的。简称V-Table。 在这个表中,主要是一个类的虚函数的地址表,这张表解决了 继承、覆盖的问题,保证内容真实反映实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子 类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。C++的编译器保证虚函数表的指针存在与对象实例中的最前面 的位置(这是为了保证取道虚表函数的高性能)。
在每一个包含虚函数的类中,都存在一张虚函数表,用来记录虚函数真正的地址。当派生类继承于基类时,将基类的虚函数表也一同继承,然后根据自己是否覆盖基类的函数,来修改虚函数表。具体看这个:讲的很详细。实际上是每一个类都会创建一张虚表,每一个类的实例也就从类中得到了一张虚表.
一般: 无覆盖
一般继承: 虚函数覆盖
多重继承:无覆盖
可以看到子类函数的虚表就接在第一个基类虚表的后面
多重继承: 有虚函数覆盖
需要注意的问题:
不能通过父类型的指针访问子类自己的函数。即使这种目的可以使用指针的方式访问来达到
包含任一纯虚函数的类 为 抽象类。抽象类是不能够实例化的。
#include <iostream> #include <cstdio> using namespace std; class Base{ public: virtual void f(){ cout<<" Base::f "<<endl; } virtual void g(){ cout<<" Base::g "<<endl; } virtual void h(){ cout<<" Base::h "<<endl; } }; class Base1{ public: virtual void f(){ cout<<" Base1::f "<<endl; } virtual void g(){ cout<<" Base1::g "<<endl; } virtual void h(){ cout<<" Base1::h "<<endl; } }; class Base2{ public: virtual void f(){ cout<<" Base2::f "<<endl; } virtual void g(){ cout<<" Base2::g "<<endl; } virtual void h(){ cout<<" Base2::h "<<endl; } }; class Base3{ public: virtual void f(){ cout<<" Base3::f "<<endl; } virtual void g(){ cout<<" Base3::g "<<endl; } virtual void h(){ cout<<" Base3::h "<<endl; } }; class Derive : public Base{ public: virtual void f1(){ cout<<" Derive::f1 "<<endl; } virtual void g1(){ cout<<" Derive::g1 "<<endl; } virtual void h1(){ cout<<" Derive::h1"<<endl; } }; class Derive1 : public Base{ public: virtual void f(){ cout<<" Derive1::f"<<endl; } virtual void g1(){ cout<<" Derive1::g1 "<<endl; } virtual void h1(){ cout<<" Derive1::h1"<<endl; } }; class Derive2 : public Base1, public Base2, public Base3{ public: virtual void f1(){ cout<<" Derive2::f1"<<endl; } virtual void g1(){ cout<<" Derive2::g1 "<<endl; } }; class Derive3: public Base1, public Base2, public Base3{ public: virtual void f(){ cout<<" Derive3::f"<<endl; } virtual void g1(){ cout<<" Derive3::g1 "<<endl; } }; typedef void(*Fun)(void); //作为函数指针 你得规定好指向的函数的参数类型,返回值类型. int main(void) { Base b; Fun pFun = NULL; cout<<"虚函数表地址: "<<&b<<endl; cout<<"虚函数表地址: "<<(void *)&b<<endl; cout<<"虚函数表地址: "<<(int*)&b<<endl; cout<<"虚函数表地址: "<<(long*)&b<<endl; /*上面四种输出结果一致 , b的地址转换成何种类型的指针看似是无所谓*/ cout<<"虚函数表---第一个函数地址: "<<(int *)*(int *)(&b)<<endl; cout<<"虚函数表---第一个函数地址: "<<(void *)*(int*)(&b)<<endl; /* * long:在32位系统是32位整型,取值范围为-2^31 ~ (2^31 - 1);在64位系统是64位整型,取值范围为-2^63 ~ (2^63 - 1) * 为了能在64位机器上运行,这里要用 long 而不是int */ pFun = (Fun)*((long *)*(long*)(&b)); //Base::f pFun = (Fun)*((long *)*(long*)(&b) + 1); //Base::g pFun = (Fun)*((long *)*(long*)(&b) + 2); //Base::h /* 我们就可以通过上面的方法调用函数 */ /* *虚函数表明明显是在子类继承父类的时候能够发挥作用, 这里继承可以分为两类: * 一般继承(无虚函数覆盖 & 有虚函数覆盖) 和 多重继承(有 & 无 虚函数覆盖) */ /* 一般: 无覆盖*/ Derive d; pFun = (Fun)*((long *)*(long*)(&d) + 3); //Derive::f1 /* * 1)虚函数按照其声明顺序放于表中。 * 2)父类的虚函数在子类的虚函数前面。 */ /* 一般继承: 有虚函数覆盖 */ Derive1 d1; pFun = (Fun)*((long *)*(long*)(&d1)); //Derive::1 pFun = (Fun)*((long *)*(long*)(&d1) + 1); //Base::g /* * 1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。 * 2)没有被覆盖的函数依旧。 */ /* 多重继承: 无覆盖*/ Derive2 d2; pFun = (Fun)*((long *)*(long *)(&d2)); //Base::f pFun = (Fun)*((long *)*(long *)(&d2) + 3); //Derive2::f pFun = (Fun)*((long *)*( (long *)(&d2) + 1)); // Base2: f 这里*是取地址得到了虚表上第一个虚函数表Base1中第一个函数的地址 ; + 1 得到第二个虚函数表Base2中第一个函数的地址; /* *1) 每个父类都有自己的虚表。 *2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的) */ /* 多重继承: 有虚函数覆盖*/ Derive3 d3; pFun = (Fun)*((long *)*( (long *)(&d3) ) ); //Derive3: f pFun = (Fun)*((long *)*( (long *)(&d3) + 1)); // Derive3: f pFun = (Fun)*((long *)*( (long *)(&d3) + 2)); // Derive3: f 这里*是取地址得到了虚表上第一个虚函数表Base1中第一个函数的地址 ; + 1 得到第二个虚函数表Base2中第一个函数的地址; /*这里设计到了虚表的概念,也就是每一个基类在子类的虚表中都占有一席 , 我们当然可以通过上面这行code来访问其他虚表, *但是还可以用二维数组来访问,更加便捷 */ long **pVtab = (long **)&d3; //Base1 's vtable pFun = (Fun)pVtab[0][0];//Derive3::f pFun = (Fun)pVtab[0][1];//Base::g //Derive's vtable pFun = (Fun)pVtab[0][3]; //Derive3::g1 //The tail of the vtable pFun = (Fun)pVtab[0][4]; /* *三个父类虚函数表中的f()的位置被替换成了子类的函数指针。 *我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。 *这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如: */ Base1 *b1 = &d3; Base2 *b2 = &d3; Base3 *b3 = &d3; b1->f(); //Derive3::f() b2->f(); //Derive3::f() b3->f(); //Derive3::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g() /* 当然,父类指针只能指向子类中覆盖的父类中的虚函数, 如下,尝试访问子类中自有的函数 会报错*/ Base1 *b4 = new Derive3(); //b4 -> g1();///error: ‘class Base1’ has no member named ‘g1’| /* 但是并不安全: * 1) 通过父类型的指针访问子类自己的虚函数: 在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。 * 2) 访问non-public的虚函数: */ return 0; }
#########################################################################################################
#再说C++多态 #
#########################################################################################################
. “C++的多态性用一句话概括就是:在基类的函数前+vritual关键字,在派生类中重写该函数,运行时会根据对象的实际类型来调用相应的函数(晚绑定或者叫动态绑定)。如果对象类型是基类,那么调用基类的函数。如果是派生类,那么调用派生类的函数。”
1:用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
2:存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
3:多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
4:多态用虚函数来实现,结合动态绑定.
5:纯虚函数是虚函数再加上 = 0;
6:抽象类是指包括至少一个纯虚函数的类。
纯虚函数:virtual void fun()=0;即抽象类!必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。
编译器在编译的时候,发现Base类中有虚函数,这时候编译器会为每个包含虚函数的类创建一个虚表(vtable),该表是一个一维数组,在这个数组中存放每一个函数的地址。
如何定位虚表?编译器提供另外一个结构虚指针(vptr)。虚指针指向虚表,虚表中存放的是函数的地址。在程序运行时,根据对象的类型去初始化vptr,从而让vptr指向正确的所属类的虚表。由于pFather实际指向的对象类型是Son,所以vptr指向Son的vtable。