目录
1.多态的概念
2.多态的定义及实现
2.1多态构成的条件
2.2虚函数
2.3虚函数的重写
2.4C++11override和final
2.5重载、覆盖(重写)、隐藏(重定义)的对比
3.抽象类
3.1概念
3.2接口继承和实现继承
4.多态的原理
4.1虚函数表
4.2多态的原理
5.单继承和多继承关系的虚函数表
5.1单继承的虚函数表
5.2多继承的虚函数表
5.3打印虚表
1.多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如:抢红包
多态分为:静态多态和动态多态
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态。
动态绑定又称后期绑定(晚绑定),是在运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数,也成为动态多态。
静态多态:函数重载,看起来调用同一个函数有不同行为。静态多态是在编译时实现的。
cout可以输出不同类型的数据,使用的是函数重载。这就是静态多态。重载是编译时完成的。
#includeusing namespace std; int main() { int i = 1; double d = 2.22; cout << i; //cout.operator<<(int) cout << d;//cout.operator<<(double) return 0; }
动态多态:一个父类的引用或指针去调用同一个函数,传递不同对象,会调用不同的函数。动态多态是在运行时实现的。
#includeusing namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: //子类中满足三同(函数名、参数、返回值)虚函数(virtual修饰),叫做重写(覆盖) virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket();//这里构成了多态 } int main() { Person ps; Student st; Func(ps);//传父类对象,调用父类的虚函数 Func(st);//传子类对象,调用子类的虚函数 return 0; }
对象不同结果不同:
2.多态的定义及实现
2.1多态构成的条件
构成多态有两个条件:
想要构成多态必须满足这两个条件!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写。
构成多态时:传哪个类型的对象,调用的就是这个类型的虚函数--跟对象有关
不构成多态:调用就是p类型的函数。--跟类型有关。
问题1:当不是基类的指针或引用的时候,也就不构成多态。
#includeusing namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person p)//不构成多态的时候,调用那个函数跟p的类型有关。是基类类型就调用基类的函数,反之调用派生类的 { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
和p的类型相关:
2.2虚函数
虚函数:即被virtual修饰的类非静态成员函数称为虚函数,其它函数不能成为虚函数。
class Person
{
public:
virtual void BuyTicket() //虚函数
{
cout << "买票-全价" << endl;
}
};
2.3虚函数的重写
重写4个条件:必须是虚函数、三同。重写是针对派生类来说的。
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() //重写(覆盖)
{
cout << "买票-半价" << endl;
}
};
问题2:当不构成重写,也就不构成多态了,和p的类型相关
#includeusing namespace std; class Person { public: void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p)//不构成多态的时候,调用那个函数跟p的类型有关。是基类类型就调用基类的函数,反之调用派生类的 { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
问题3:当参数不同时,不构成重写,也就不构成多态。和p的类型相关
#includeusing namespace std; class Person { public: virtual void BuyTicket(int a) { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket(char b) { cout << "买票-半价" << endl; } }; void Func(Person& p)//不构成多态的时候,调用那个函数跟p的类型有关。是基类类型就调用基类的函数,反之调用派生类的 //这里如果改成student& 当传的是基类对象时(需要强制类型转换)打印的是基类的函数,那这样和p的类型相关就冲突了啊,但是这样会出现越界的,就是错误的传送方法。所以不要多考虑 { p.BuyTicket(10); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
问题4:当返回值不同时不构成重写,则不构成多态。
#includeusing namespace std; class Person { public: virtual int BuyTicket() { cout << "买票-全价" << endl; return 0; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p)//不构成多态的时候,调用那个函数跟p的类型有关。是基类类型就调用基类的函数,反之调用派生类的 { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
但是报错了:
错误信息:不是协变。
虚函数重写的两个例外:
1.协变(基类与派生类虚函数返回值类型不同)
重写要求返回值相同有一个例外:协变--要求返回值是父子关系的指针或引用。
返回值不同,也构成了多态:协变,(了解一下)
1.可以是自己的类型指针或引用
#includeusing namespace std; class Person { public: virtual Person& BuyTicket()//返回值是自己的类型 { cout << "买票-全价" << endl; return Person(); } }; class Student : public Person { public: virtual Student& BuyTicket() { cout << "买票-半价" << endl; return Student(); } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
2.可以是其他父子关系的类型指针或引用
#includeusing namespace std; class A{}; class B:public A {}; class Person { public: virtual A& BuyTicket() //返回值是其它类型的,满足父子关系的指针或引用 { cout << "买票-全价" << endl; return A(); } }; class Student : public Person { public: virtual B& BuyTicket() { cout << "买票-半价" << endl; return B(); } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
析构函数名字不同,但是构成了重写。因为编译后析构函数的名称统一处理成destructor。
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
析构函数对于普通对象:
构成重写时:
#includeusing namespace std; class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; int main() { Person p; Student s; return 0; }
不构成重写时:
#includeusing namespace std; class Person { public: ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() { cout << "~Student()" << endl; } }; int main() { Person p; Student s; return 0; }
总结:普通对象,析构函数是否虚函数,是否完成重写,都正确调用了析构函数。对普通对象没影响。
不管你是虚函数还是重写,都是不影响普通对象的:
#includeusing namespace std; class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; int main() { Person p; Student s; return 0; }
析构函数对于非普通对象:
但是如果不是普通对象呢?
#includeusing namespace std; class Person { public: ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p1 = new Person; //operator new() +构造函数 Person* p2 = new Student; delete p1;//析构函数+operator delete() p1->destructor() delete p2;//p2->destructor() return 0; }
发现只调用了父类的析构函数,只和p1和p2的类型有关系,因为时Person* 类型的,所以调用父类的。此时p2中就存在内存泄露,仅仅析构了派生类中的Person的部分,Student部分没有完成析构。若是Student不需要清理资源还好,要是需要清理资源,那就造成了内存泄漏。
所以此时需要多态来完成:p1->destructor() 和p2->destructor() 满足了 是基类的指针,virtual ~Person 和virtual ~Student 满足重写。 所以满足了多态。
#includeusing namespace std; class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p1 = new Person; //operator new() +构造函数 Person* p2 = new Student; //主要是这个对象需要多态。既需要释放自己的,也要释放Person继承下来的 delete p1;//析构函数+operator delete() delete p2; return 0; }
总结:动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数,完成重写构成多态,才能正确调用析构函数。
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写:这个不是很规范,建议都写上。
虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足三同,就构成重写。
#includeusing namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: void BuyTicket() //这里不写virtual也是虚函数,也是和基类的构成重写。虽然子类没写virtual它是先继承了父类虚函数的属性,再完成重写 { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
当子类的虚函数设置成私有的时候:为什么还能调的到?
这里继承的是父类虚函数的属性(public和外壳 virtual void BuyTicket),重写的是内容(cout<<“”的内容)。
#include本质上,子类重写的虚函数,可以不加virtual是因为析构函数,大佬设计初衷,父类析构函数加上了virtual ,那么就不存在不构成多态,没调用子类析构函数,内存泄漏的场景。父类加了,子类没加virtual。using namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { private: //当这里是私有的时候。 void BuyTicket() //这里是私有的为什么还能调的到? { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
2.4C++11override和final
如何设计一个不能被继承的类?
在C++98中是将构造函数设置成私有:间接限制 ,子类构造函数无法调用父类构造函数初始化成员,无法实例化对象。
#includeusing namespace std; class A { private: //构造函数私有,就无法被继承了 A(int a=0) :_a(a) { } int _a; }; class B:public A { }; int main() { B b; return 0; }
设置成私有后A类也没办法创建对象了,解决方法,通过静态成员函数:静态成员函数不需要对象就可以调用 类域::函数。并且没有this指针:
#includeusing namespace std; class A { public: static A Creat_A(int a=0) { return A(a); } private: //构造函数私有,就无法被继承了 A(int a=0) :_a(a) { } int _a; }; class B:public A { }; int main() { A a=A::Creat_A(20); return 0; }
1.final:
1.1在C++11中,final修饰类,该类不能被继承
#includeusing namespace std; class A final //A类不能被继承 { private: int _a; }; class B:public A { }; int main() { return 0; }
继承时会报错:
1.2C++11中,final 修饰虚函数,该虚函数不能被重写
#includeusing namespace std; class A { public: virtual void fun() final //该虚函数不能被重写 { cout << "fun()" << endl; } private: int _a; }; class B:public A { virtual void fun() { cout << "FUN()" << endl; } };
重写,直接报错:
2.override:检查派生类虚函数是否重写了某类某个虚函数,如果没有重写编译报错。
override放在子类重写的虚函数的后面,检查是否完成重写,没有重写就会报错
#includeusing namespace std; class A { public: virtual void fun() //该虚函数不能被重写 { cout << "fun()" << endl; } private: int _a; }; class B:public A { virtual void fun() override { cout << "FUN()" << endl; } };
2.5重载、覆盖(重写)、隐藏(重定义)的对比
3.抽象类
3.1概念
在虚函数的后面写上 =0,则这个函数为纯虚函数。纯虚函数一般只声明,不实现。实现没有价值,因为这个类直接就无法实例化出对象。
class A
{
public:
virtual void fun() = 0; //纯虚函数
};
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。但是可以定义指针或引用,使用指针和引用调用纯虚函数会报错。为什么会报错,看多态原理。
#includeusing namespace std; class A { public: virtual void fun() = 0//纯虚函数 { cout << "fun()" << endl; } }; int main() { A* p = nullptr; p->fun();//使用指针调用纯虚函数,会报错。 return 0; }
派生类继承后也不能实例化处对象,只能重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数再体现出了接口继承。
#includeusing namespace std; class A { public: virtual void fun() = 0;//纯虚函数 }; class B :public A { public: virtual void fun() //必须重写基类的纯虚函数才能创建对象 { cout << "Fun()" << endl; } }; int main() { B b; return 0; }
什么是抽象?什么类可以设置成抽象类?
抽象:在显示时间中没有对应的实物
一个类型,如果一般在现实世界中,没有具体的对象实物就定义成抽象类比较好。比如:人。
3.2接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不是实现多态,不能把函数定义成虚函数。
4.多态的原理
4.1虚函数表
求这个类实例化出对象的大小:
#includeusing namespace std; class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; char ch = ' '; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }
其实在实例化对象中多了一个指针,虚函数表指针:
定义2个虚函数:
#includeusing namespace std; class Base { public: virtual void Func1() { cout << "Func1()" << endl; } virtual void Func2() { cout << "Func2()" << endl; } private: int _b = 1; char ch = ' '; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }
这段代码报错是因为,我需要通过解引用p去查找存在对象中虚函数表指针以实现多态,p是nullptr,所以会报错。
#includeusing namespace std; class A { public: virtual void fun() = 0//纯虚函数 { cout << "fun()" << endl; } }; int main() { A* p = nullptr; p->fun();//使用指针调用纯虚函数,会报错。 return 0; }
4.2多态的原理
使用以下代码进行讲解:
#includeusing namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } private: int _a; }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } private: int _b; }; void Func(Person& p) { p.BuyTicket(); } int main() { Person Mike; Func(Mike); Student Johnson; Func(Johnson); return 0; }
1.紫色箭头:p指向Mike对象,并通过p->BuyTicket() 在Mike对象中的虚表指针找到虚表,再通过虚表里的虚函数指针找到基类的虚函数,从而调用基类的虚函数。
2.红色箭头:p指向Johnson对象,这里发生了切片,把派生类的虚函数指针给了p(这里就是重写/覆盖了),并通过p->BuyTicket() 在派生类中继承的父类中找到虚函数表指针,并通过虚函数表指针找到虚函数表,再通过虚函数表找到对应的虚函数。
为什么必须使用父类的引用或指针,不能是对象呢?
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
Person& p = Johnson;//引用会把虚表指针+成员变量的值都切片过去
Person b = Johnson; //对象不会把虚表指针切片切过去,只会把值切片过去
return 0;
}
对比他们的虚表指针:发现基类引用与派生类对象之间在切片的时候,会把虚函数表指针一块切片过去赋值给基类。但是基类对象与派生类对象之间,发生切片不会把虚表指针切片赋值给基类 ,虚函数表指针还是父类的虚表指针没有改变。
同类型的虚函数表指针是一样的,都是同一个虚函数表,同一个虚函数,虚函数和普通函数存的地方都是一样的,都存在代码段。只是虚函数要把地址存到虚表,方便实现多态。普通函数在编译的时候就确定了地址,构成多态,虚函数是在运行的时候去查找地址。
#includeusing namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } private: int _a; }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } private: int _b; }; void Func(Person& p) { p.BuyTicket(); } void fun() { cout << "fun()" << endl; } int main() { //构成多态时,运行时确定虚函数地址 Person Mike; Func(Mike); Student Johnson; Func(Johnson); //不够成多态,编译时就确定地址 Person p1 = Mike; Person p2 = Johnson; p1.BuyTicket(); p2.BuyTicket(); return 0; }
构成多态后,运行起来,根据传入的对象确定虚函数地址。构成多态后,所以运行起来以后确定虚函数地址。
不构成多态,虚函数和普通函数一样,在编译的时候就确定了地址:
同类型的虚函数表指针是一样的:
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
Person p1;
Person p2;
return 0;
}
当子类虚函数设置成私有后,为什么还能调的到?
#includeusing namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } private: int _a; }; class Student : public Person { private: //把子类的虚函数设置成私有的 virtual void BuyTicket() { cout << "买票-半价" << endl; } private: int _b; }; void Func(Person& p) { p.BuyTicket(); } int main() { Student s; Func(s); return 0; }
原因:这里是接口继承,仅仅是重写父类中的虚函数的内容,属性不会改变(函数外壳是不会改变的,改变的函数里面的实现内容)。当子类对象传给父类引用的时候,会把虚函数表指针改变,因此改变了原来虚函数表指针的指向,在找虚函数的时候需要去找虚函数表指针,但是父类中虚函数的公共属性是不会改变的。
使用以下代码进行讲解和分析几点问题:
#includeusing namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }
在基类对象b中,只有虚函数的地址才会放到虚函数表里,Func3不是虚函数,不会放入虚函数表中。同理,在子类中,Func2继承下来后是虚函数,所以放在了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表:
当子类对象d,在创建的时候,会把基类的虚函数表里面的虚函数地址拷贝一份,当子类完成重写的虚函数比如Func1,会用子类自己的虚函数地址覆盖拷贝下来的原虚表里的地址。没有重写的虚函数,虚函数地址还是会继续放在子类的虚函数表里。这里Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
若是派生类d自己新增加的虚函数按其在派生类中声明次序增加到派生类虚表的最后:
#includeusing namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl;//Func1完成了重写 } virtual void Func4()//派生类自己的虚函数 { cout << "Derive::Func4()" << endl; } private: int _d = 2; }; void Fun(Base& b) { b.Func2(); } int main() { Base b; Derive d; Fun(d); return 0; }
在监视表里看不出来,需要去内存看虚表指针的地址,Func4放在了虚表的后面:
虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr。
接着看:
#includeusing namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl;//Func1完成了重写 } private: int _d = 2; }; int main() { Base b; Derive d; Base* p1 = &b; p1->Func1(); Base*p2 = &d; p2->Func1(); return 0; }
继续分析Func1,不管是基类还是派生类,虚表指针指向的内存中Func1的地址和取地址虚函数Func1的地址不一样:
p1->Func1处打断点,其实这里对地址进行多一层的封装,通过反汇编:
在Call eax处 按下F11,可以看到,其实eax中存的是jmp 指令的地址,最终jmp会去调用虚函数,这里封装类一层,但是最后都会找到对应虚函数的地址,并正确调用虚函数,所以实际虚函数的真正地址是&Base::Func1,而虚函数表中真正存储的是jmp指令地址,性质都一样,只不过封装了一层。
p2->Func1同理。
这里还有一个问题:虚函数存在哪里?虚表存在哪里?
需要注意虚表存的是虚函数指针,不是函数,虚函数和普通函数一样的,都是存在代码段的,只是虚函数的指针存在了虚表中。另外对象中存的不是虚表,存的是虚表指针。其实虚表是存在代码段的。
在操作系统中常量区和代码段是合在一起的叫代码段,常量区是从语言角度说的。
#includeusing namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl;//Func1完成了重写 } private: int _d = 2; }; int main() { int*p = (int*)malloc(4); printf("堆:%pn",p); int a = 0; printf("栈:%pn", &a); static int b = 0; printf("数据段:%pn", &b); const char*p1 = "aaaaa"; printf("常量区:%pn", p1); printf("代码段:%pn", &Base::Func1); Base d; printf("虚函数表:%pn",*((int*)&d)); return 0; }
虚函数表存在常量区(代码段) :
5.单继承和多继承关系的虚函数表
5.1单继承的虚函数表
#include
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
#includeusing namespace std; class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; int main() { Base b; Derive d; return 0; }
5.2多继承的虚函数表
#include
using namespace std;
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
int main()
{
Derive d;
return 0;
}
在d中存在两个虚表,重写了func1,但是没有重写func3,首先func3存在哪里了?func1重写后在虚表中是什么样的?
多继承后的d是存在两个虚表的,那func3是存在哪里呢?
在监视窗口中看不到,算是VS的BUG隐藏了。使用内存观察虚表的内容:
第一张虚表:
第二张虚表:
答案:观察上面的监视窗口,func3是存在第一张虚表中。
再看func1在虚表中是什么样的?存在两张虚表中。
为什么d重写了func1 在两个虚表中的地址为什么不一样呢?其实这里进行了一层封装,是对jmp指令的封装,最终jmp都会跳转到d中重写的func1的地址处。
使用以下代码验证:
#includeusing namespace std; class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; int main() { Derive d; Base1*p1 = &d; p1->func1(); //在这里打断点,转汇编,观察 Base2*p2 = &d; p2->func1(); //在这里打断点,转汇编,观察 return 0; }
打完断点后,转汇编,观察:
首先观察,p1->func1():
按下F11进入call:
上面的p1->func1(),最终func1的地址是0x0DE5560 ,再观察p2->func1():
按下F11后 跳转到了jmp指令:
继续按下F11:
继续执行按下F11,又回到了一开始p1->func1()的jmp地址处:
总结:虚表中存的是jmp指令的地址,地址必然不同,但是最后jmp指令跳转的func1地址是一样的。所以是指向同一个虚函数func1的。
考虑一个问题:虚函数表指针是什么时候进行初始化赋值的?
调试一下代码,发现虚表指针是在初始化列表进行初始化赋值的。
#includeusing namespace std; class Base { public: Base() { cout << "Base()" << endl; } virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; int main() { Base b;//在此处打断点 ,观察。 Derive d; return 0; }
5.3打印虚表
1.单继承
#includeusing namespace std; class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int b; }; typedef void(* Vf_Ptr )(); void PintfVFTable(Vf_Ptr* table) //函数指针的指针 是个二级指针 { //打印虚表里面的虚函数地址 //需要注意这里打印这个虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面 //没有放nullptr,导致越界,这是编译器的问题,我们只需要目录栏的-生成-清理解决方案,在编译就好了 for (int i = 0; table[i] != nullptr; i++) { printf("第%d虚函数地址:0x%pn",i,table[i]); Vf_Ptr tmp = table[i]; tmp();//调用虚函数。 } cout << endl; } int main() { Base b; Derive d; PintfVFTable((Vf_Ptr*)(*(int*)&b));//虚表指针取前4个字节就可以,把地址取出来。 PintfVFTable((Vf_Ptr*)(*(int*)&d)); return 0; }
把虚函数设置成私有,还能不能打印?
#includeusing namespace std; class Base { private://设置成私有成员 virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { private://设置成私有成员 virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int b; }; typedef void(* Vf_Ptr )(); void PintfVFTable(Vf_Ptr* table) { for (int i = 0; table[i] != nullptr; i++) { printf("第%d虚函数地址:0x%pn",i,table[i]); Vf_Ptr tmp = table[i]; tmp();//调用虚函数。 } cout << endl; } int main() { Base b; Derive d; PintfVFTable((Vf_Ptr*)(*(int*)&b));//虚表指针取前4个字节就可以,把地址取出来。 PintfVFTable((Vf_Ptr*)(*(int*)&d)); return 0; }
设置成私有的还是可以打印,因为我这里是直接找的地址,根本就没有对象的事,访问限定符限制的是常规操作,我这里是非常规的操作,并且这里调用虚函数的时候也没有传this指针,我也没有访问对象的成员数据,所以不会崩。 打破了规则。像小偷进入你家一样。
上面的代码切换到64位就不行了,程序崩了,因为在64上,地址的是8个字节进行编址了:
所以需要这样改:
int main()
{
Base b;
Derive d;
PintfVFTable((Vf_Ptr*)(*(long long*)&b));
PintfVFTable((Vf_Ptr*)(*(long long*)&d));
return 0;
}
long long 切换到32位时也是可以的,为什么?
int main()
{
Base b;
long long tmp = (*(long long*)&b);
PintfVFTable((Vf_Ptr*)(*(long long*)&b));
return 0;
}
long long 是8个字节的大小,在解引用取值时,会把对象b里面虚表指针和a的值也取上:
当执行(Vf_Ptr*) 强制类型转换时又变成了4个字节,需要截断正好取的后面的4个字节。
强转只能是意义相关类型才可以:int和char 整数、int和double 表示数据大小、整型 和指针。
所以以下处理是最好的:
这里可以这样替换,为了解决32位和64位的对应字节不同而导致的打印错误,32位地址是4个字节,但是64位地址是8个字节导致编址不一样会出错。
PintfVFTable((Vf_Ptr*)(*(int*)&b)); //可以修改为 PintfVFTable((Vf_Ptr*)(*(void**)&b)); //不能对void*进行解引用,但是这里是对void**解引用。 PintfVFTable((Vf_Ptr*)(*(char**)&b));//也可以这样或int** 等等,但是不推荐 //*(int*) 看的是int的大小4个字节 //*(long long*) 看的是long long 的大小 8个字节 //*(void**) 看的是void** 的大小,在32位是4个字节,在64位是8个字节。正好解决了32位和64位发生截断的问题。 //个人理解:就是为了提供一个自己需要的空间大小。
也可以使用条件编译:
在win32中,定义了_WIN32,没有定义_WIN64。在win64中,定义了_WIN32和_WIN64。
int main()
{
Base b;
#ifdef _WIN64
PintfVFTable((Vf_Ptr*)(*(long long*)&b));
#else
PintfVFTable((Vf_Ptr*)(*(int*)&b));
#endif
return 0;
}
2.多继承
#includeusing namespace std; class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; typedef void(*Vf_Ptr)(); void PintfVFTable(Vf_Ptr* table) { for (int i = 0; table[i] != nullptr; i++) { printf("第%d虚函数地址:0x%pn", i, table[i]); Vf_Ptr tmp = table[i]; tmp();//调用虚函数。 } cout << endl; } int main() { Derive d; //第一种写法 //PintfVFTable((Vf_Ptr*)*(void**)&d);//打印第一张虚表 //cout << endl; //PintfVFTable((Vf_Ptr*)*(void**)((char*)&d+sizeof(Base1)));//打印第二张虚表,需要跳过Base1 //cout << endl; //可以这样 //第二种写法 Base1* p1 = &d; Base2* p2 = &d;//这里会自己去偏移,&d一开始的地址是指向起始位置的,这里编译器会自己寻找Base2的偏移地址。 PintfVFTable((Vf_Ptr*)*(void**)p1); //打印第一张虚表 cout << endl; PintfVFTable((Vf_Ptr*)*(void**)p2);//打印第二张虚表 return 0; }
这里的Base1* p1 = &d和Base2* p2 = &d; ,在发生切片的时候,编译器会自己去寻找偏移Base2的偏移地址,先继承的Base1再继承的Base2,一开始指向Base1(起始地址),编译器自己会计算偏移量去找Base2。
2.1多继承中的菱形继承和菱形虚继承
#includeusing namespace std; class A { public: virtual void fun() { } 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; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
D对象中,B和C中存的是虚基表指针(存的偏移量)以及成员变量_b的值 ,D中存的成员变量的值,A中存的是虚函数表指针(A的虚函数表指针)和变量_a的值。
当B和C都重写fun后,如果D不重写会报错。因为继承下来后,D对象中A是公共的,里面的虚表指针不知道是使用B中的fun重写(覆盖),还是使用C的fun重写(覆盖):
class A
{
public:
virtual void fun()
{
}
public:
int _a;
};
class B:virtual public A
{
public:
virtual void fun()
{
}
public:
int _b;
};
class C :virtual public A
{
public:
virtual void fun()
{
}
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
所以要求D必须重写:
#includeusing namespace std; class A { public: virtual void fun() { cout << "A" << endl; } public: int _a; }; class B: virtual public A { public: virtual void fun() { cout << "B" << endl; } public: int _b; }; class C : virtual public A { public: virtual void fun() { cout << "C" << endl; } public: int _c; }; class D : public B ,public C { public: virtual void fun() //B和C都重写的,D必须重写 { cout << "D" << endl; } public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 1; d._b = 3; d._c = 4; d._d = 5; return 0; }
当B和C中有自己的虚函数并且重写了A中的fun的时候:
#includeusing namespace std; class A { public: virtual void fun() { cout << "A" << endl; } public: int _a; }; class B: virtual public A { public: virtual void fun() { cout << "B" << endl; } virtual void fun1() //B自己的虚函数 { cout << "B" << endl; } public: int _b; }; class C : virtual public A { public: virtual void fun() { cout << "C" << endl; } virtual void fun1()//C自己的虚函数 { cout << "B" << endl; } public: int _c; }; class D : public B ,public C { public: virtual void fun() { cout << "D" << endl; } public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 1; d._b = 3; d._c = 4; d._d = 5; return 0; }
有3张虚表,B和C自己的以及公共的A:
看一下B虚基地址,里面存的什么:
暑期编程PK赛 得CSDN机械键盘等精美礼品!


