总言
主要介绍多态相关内容。
文章目录
- 总言
- 1、多态介绍
- 1.1、多态是什么
- 1.2、构成多态的两个必备条件
- 1.2.1、虚函数介绍
- 1.2.2、基类的指针或者引用调用虚函数
- 1.2.3、演示多态条件的破坏(两个特例说明)
- 1.2.3.1、不符合条件演示
- 1.2.3.2、特例演示
- 1.3、一道例题讲解
- 2、多态原理
- 2.1、虚函数表
- 2.2、条件破坏
- 2.3、再识虚函数
- 2.3.1、析构函数与virtual重写
- 2.3.2、C++11两个关键字:final && override
- 2.3.3、概念比较:函数重写、函数重载、函数重定义
- 3、抽象类、接口继承
- 3.1、抽象类介绍
- 3.2、接口继承和实现继承
- 4、单继承和多继承关系中的虚函数表
- 4.1、单继承中虚函数表
- 4.2、多继承中虚函数表
1、多态介绍
1.1、多态是什么
1)、基本概念
多态:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
1.2、构成多态的两个必备条件
1)、总述
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要构成多态还有两个条件:
1、必须通过基类的指针或者引用调用虚函数
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
1.2.1、虚函数介绍
1)、多态条件之一:虚函数概念
虚函数: 即被关键字virtual
修饰的类成员函数,称为虚函数。
需要注意:
1、virtual
关键字有两用:其一,在继承关系中用于表示虚继承;其二,修饰类成员函数用于表示虚函数。
class Person
{
public:
string _name;
};
class Student : virtual public Person//此处表示虚继承
{
protected:
int _num;
};
class Person {
public:
virtual void BuyTicket() //类成员函数:虚函数
{
cout << "买票-全价" << endl;
}
};
2、虚函数必须是类成员函数才行。
2)、虚函数重写
虚函数的重写(覆盖): 派生类中有一个跟基类完全相同的虚函数,称子类的虚函数重写了基类的虚函数。
注意达成重写的条件: 派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同。(若是不构成重写,当函数名相同时,父子类该函数关系为隐藏。)
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
1、有父子类:说明有继承关系,可以看到此处Student、Solider
继承了Person
类
2、子类的虚函数重写了父类的虚函数:virtual void BuyTicket()
,是虚函数、函数名、参数、返回值相同,构成重写。
3)、虚函数的两个特例
1、特例一:重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写。
2、特例二:重写协变。当派生类重写基类虚函数时,若要在与基类虚函数返回值类型不同的情况下保持虚函数重写关系,则要求返回类型必须是父子关系的指针或者引用,即:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,将其称为协变。
此两个特例将在后续1.2.3中演示。
1.2.2、基类的指针或者引用调用虚函数
1)、多态条件之一:基本说明
以下述代码为例:void Func(Person& p)
:可看到此处形参p的类型是父类引用。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sl;
Func(ps);
Func(st);
Func(sl);
return 0;
}
2)、结果演示
在构成多态的情况下:当具备多态的两个条件时,我们去调用相关函数Func
,结果如下:根据我们传入的不同类型,ps、st、sl
分别得到不同结果,这就是多态,不同继承关系的类对象,去调用同一函数,产生了不同的行为。
假如没有多态,单看下述这个函数,我们使用ps、st、sl
去调用,由于p的类型为Person &
,st、sl
调用时存在切片行为,因此我们将得到父类中的BuyTicket
。
void Func(Person& p)
{
p.BuyTicket();
}
1.2.3、演示多态条件的破坏(两个特例说明)
1.2.3.1、不符合条件演示
以下述代码为例:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
void test01()
{
Person ps;
Student st;
Soldier sl;
Func(ps);
Func(st);
Func(sl);
}
1)、演示一:破坏基类指针/引用
void Func(Person p)
{
p.BuyTicket();
}
可看到多态被破坏。
2)、演示二:虚函数参数不同
class Person {
public:
virtual void BuyTicket(int) { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket(char) { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
virtual void BuyTicket(size_t) { cout << "买票-优先" << endl; }
};
void Func(Person& p)
{
p.BuyTicket(1);
}
可看到多态被破坏。
1.2.3.2、特例演示
3)、演示三:子类虚函数不加virtual关键字
在重写基类虚函数时,派生类的虚函数在不加virtual
关键字时,也可以构成重写。因为虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,故而基类的虚函数被继承下来,在派生类依旧保持虚函数属性。但是该种写法并不规范,一般不建议这样使用。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
//virtual void BuyTicket() { cout << "买票-半价" << endl; }
void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
//virtual void BuyTicket() { cout << "买票-半价" << endl; }
void BuyTicket() { cout << "买票-优先" << endl; }
};
可以看到多态仍旧成立。
4)、演示四:虚函数返回类型不同,使用父子关系指针/引用
重写协变:当派生类重写基类虚函数时,若要在与基类虚函数返回值类型不同的情况下保持虚函数重写关系,则要求返回类型必须是父子关系的指针或者引用,即:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,将其称为协变。
class Person {
public:
virtual Person* BuyTicket()
{
cout << "买票-全价" << endl;
return this;
}
};
class Student : public Person {
public:
virtual Student* BuyTicket()
{
cout << "买票-半价" << endl;
return this;
}
};
class Soldier : public Person {
public:
virtual Soldier* BuyTicket()
{
cout << "买票-优先" << endl;
return this;
}
};
这里返回的父子类型指针/引用也可以是其它类的:
class A
{};
class B : public A
{};
class C:public A
{};
class Person {
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "买票-半价" << endl;
return nullptr;
}
};
class Soldier : public Person {
public:
virtual C* BuyTicket()
{
cout << "买票-优先" << endl;
return nullptr;
}
};
1.3、一道例题讲解
1)、基本例题讲解
以下程序输出结果是什么()
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
阶段分析一:B* p = new B;
、p->test();
,p指针指向类型为B,该指针调用了函数test()。virtual void test() { func(); }
,test是A类的成员函数,对应的其隐藏的this指针为A*,实际上这里实参属于B*,形参属于A*,发生了切片行为。test()函数中调用了func()函数,实则是this->func(),A* this 进行调用,满足多态条件之一,父类指针调用虚函数。
阶段分析二:父子类中都函数func
,根据特例情况,其满足虚函数重写。那么达成多态的必备两条件,故此处属于多态调用。多态调用,要看它实际实参行为,B* p = new B;
,这里的p指向的是B类,故其会调用B类中的func。
阶段分析三:virtual void func(int val = 1)
、void func(int val = 0)
,这里A、B两类中,func的缺省参数不同,但实际上,虚函数,其在子类中的属于接口型继承,即子类接口完全继承自父类,重写的是函数实现,函数体内那一部分。
因此,此题选择B。
2)、相关衍生
假如我们做出如下修改,结果如何?
将B* p = new B;
修改为A* p = new B;
。
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
A* p = new B;
p->test();
return 0;
}
分析结果如下:
2、多态原理
2.1、虚函数表
1)、问题引入:在存在虚函数的情况下,一个类有多大?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch = 'A';
};
int main()
{
Base b;
cout<<sizeof(Base) << endl;
}
演示结果如下:
通过调试窗口,我们发现,Base中除了基本的成员变量_b、_ch
外,还多一个__vfptr
。对象中的这个指针我们叫做虚函数表指针(vf即virtual
、function
。该指针存储位置与平台有关,平台可能会放到对象的最后面)。
这说明:一个含有虚函数的类中,至少都有一个虚函数表指针,该指针指向一个虚函数表(实际上是函数指针数组),其用于存放虚函数的地址,通常,虚函数表也简称虚表。
2)、虚函数表基本说明
再次举例演示:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }//虚函数,且子类中实现了函数重写
virtual void Func() { cout << "Func" << endl; }//虚函数,但未进行函数重写
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
//验证多态:构成多态后,为什么传入对象不同,同一函数调用不同?
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
派生类的虚表生成:
1、先将基类中的虚表内容拷贝一份到派生类虚表中 。
2、如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 。
3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
注意事项:
1、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(VS下)。
2、虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中的不是虚表,存的是虚表指针。(vs下,虚表存在代码段中)
2.2、条件破坏
1)、父类指针/引用 && 非父类指针/引用
以下述代码为例:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Func" << endl; }
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
如下所示分别调用:
void Func(Person p)
{
p.BuyTicket();
}
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
可看到结果如下:
1、多态调用:运行时决议,即运行时到指向对象的虚表中调用虚函数地址,因此,指向对象是谁,就调用谁的虚函数。
2、普通调用:不构成多态,编译时就能确定调用函数的地址,运行时直接调用。
2)、破坏虚函数virtual
情况一:以BuyTicket
为例,父类无虚函数,子类设置虚函数。
class Person {
public:
//virtual void BuyTicket() { cout << "买票-全价" << endl; }
void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Func" << endl; }
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
结果:非多态调用,BuyTicket
不进入虚表中,编译时决议。
情况二:以BuyTicket
为例,父类有虚函数,子类无虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
//void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Func" << endl; }
int _a = 0;
};
class Student : public Person {
public:
//virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
结果:BuyTicket进入虚表中,只是父类和子类虚函数地址相同,说明调用时都是调用父类的虚函数,因为子类没有完成重写,故而得到结果相同。
2.3、再识虚函数
2.3.1、析构函数与virtual重写
1)、析构函数构成重写说明
建议在继承中将析构函数定义成虚函数:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同,但编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
2)、为什么建议重写
1、普通情况演示:
int main()
{
Person p;
Student s;
return 0;
}
如下图,普通情况下的调用,遵循之前我们学习的父子类析构规则,先构造的后析构,子类中先析构子类,再析构父类。
但是这种父子类中析构函数不设置为重写就能调用无误的情况有不适用的场景:
2、特殊情况演示:如下,我们创建一个父类的指针,分别调用父子类,然后析构。此时析构函数是否重写结果差异很大。
int main()
{
Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;
delete ptr2;
return 0;
}
2.3.2、C++11两个关键字:final && override
1)、final关键字
说明:
1、在继承中,如果一个类不想被继承,可加上关键字final
class A final
{
public:
A(){}
//……
protected:
int _a;
};
class B : public A //error
{
//……
};
2、在多态中,如果一个类的虚函数不想被重写,可加上关键字final
class Person {
public:
virtual void BuyTicket() final
{
cout << "买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() //error
{
cout << "买票-半价" << endl;
}
};
class Soldier : public Person {
public:
virtual void BuyTicket() //error
{
cout << "买票-优先" << endl;
}
};
2)、overide关键字
说明:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译报错。
class Person {
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() override
{
cout << "买票-半价" << endl;
}
};
class Soldier : public Person {
public:
virtual void BuyTicket(int) override
{
cout << "买票-优先" << endl;
}
};
2.3.3、概念比较:函数重写、函数重载、函数重定义
3、抽象类、接口继承
3.1、抽象类介绍
1)、纯虚函数和抽象类
概念介绍: 在虚函数的后面写上 =0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。
特点: 抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
意义: 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;//构成纯虚函数,则Car为抽象类
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
3.2、接口继承和实现继承
1)、相关介绍
实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
总结: 所以如果不实现多态,不要把函数定义成虚函数。
4、单继承和多继承关系中的虚函数表
4.1、单继承中虚函数表
1)、问题引入1:同一个父类,实例化对象,是否共用同一个虚表?
验证代码如下:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
int main()
{
Person p1;
Person p2;
return 0;
}
验证结果如下:可以看到二者共用一个虚表。同一个类型的对象共用一个虚表。
2)、问题引入2:子类继承父类,实例化对象,是否共用同一个虚表?
验证代码如下:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
Person p1;
Student s1;
return 0;
}
验证结果如下:在构成虚函数重写的情况下,子类会对相应的虚函数进行重写,且子类虚表应该存子类的虚函数,父类的虚表存父类的虚函数,故而二者有自己的虚表。
但是需要注意,在之前破坏虚函数virtual
里我们演示过,若子类没有完成虚函数重写,此时父子类虚表如下:
3)、问题引入3:同一个子类,实例化对象,是否共用同一个虚表?
验证代码如下:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
Student s1;
Student s2;
return 0;
}
验证结果如下:
4)、问题引入4:当父子类有各自的虚函数存在时,虚表内呈现效果如何?
验证代码如下:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func1() {}
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual void Func2(){}
};
int main()
{
Person p1;
Student s1;
return 0;
}
验证结果如下:
可以看到,父类和子类虚表是否共用、虚函数重写时、不重写时结果与我们之前验证的一致。但是此处有一个问题,在监视窗口里,我们似乎没在虚表中看到子类的Func2?
解决方案一: 通过内存窗口初步判断
如下述,我们在内存窗口中输入子类虚函数表指针,可以看到其虚表(函数指针数组)中内容如下:
0x002B9CF4 6a 14 2b 00 j.+.
0x002B9CF8 74 14 2b 00 t.+.
0x002B9CFC 6f 14 2b 00 o.+.
但这种方法只能粗略验证,故而我们可以使用以下方法观察:
解决方案二: 写一个函数,用来打印虚函数表
前提认识:
void(*ptr)(); //创建一个函数指针变量
typedef void(*ptr)(); //对一个函数指针变量进行重命名
相关函数如下:
typedef void (*VFPTR)();
void PrintVTable(VFPTR VTable[])
{
cout << " 虚表地址>" << VTable << endl;
for (size_t i = 0; VTable[i] != nullptr; ++i)
{
printf("VTable[%d],地址为:%p,对应函数为:",i, VTable[i]);
VTable[i]();
//等价于:
//VFPTR ptr=VTable[i];
//ptr();//通过函数指针,调用函数
}
cout << endl;
}
VFPTR VTable[]
,既然是要打印虚函数表,也就意味着要打印函数指针数组,这里形参使用的是一个函数指针。
VTable[i] != nullptr
,for循环中设置此为结束条件,是结合了VS下虚函数表最后会放置一个nullptr。
VTable[i]();
,这是函数调用,该部分涉及函数指针相关内容,可回顾C阶段所学:指针
调用写法如下:
int main()
{
Person p1;
Student s1;
VFPTR* Tablep1 = (VFPTR*)(*(int*)(&p1));
PrintVTable(Tablep1);
VFPTR* Tables1 = (VFPTR*)(*(int*)(&s1));
PrintVTable(Tables1);
return 0;
}
我们要打印虚函数表中的函数地址,地址32位下实际上为四字节,因此假如我们直接传递指向整个类的指针,那么根据指针访问权限,其一次++、--
跨步与我们的需求不一致。因此此处要进行处理。
(int*)(&p1)
,首先要明确,直接使用类型强制转换不一定能成功,比如此处的自定义类型Student和Person, 我们不能直接将其转换为int类型。但是自定义类型的指针和内置类型的指针,同属于指针,可以互相转换。
(*(int*)(&p1))
,对int*
类型的指针解引用,一次能访问4bytes
,因此我们获得对象首4bytes
的值,这个值就是指向虚表的指针。注意,这里之所以要解引用,是因为我们要获取的是对象中头四个字节的内容,而非是对象本身的地址,故而直接(int*)(&s1)
是不能满足预期的。
(VFPTR*)(*(int*)(&p1))
,强转成VFPTR*
,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
演示结果如下:可以看到,子类中的虚函数表,实际上也存储了没有重写的虚函数 。
4.2、多继承中虚函数表
1)、多态继承中虚函数表演示
演示代码如下:
Base1、Base2
为基类,Derive
分别继承二者。func1
为重写的虚函数,func2
为Base1、Base2
独自的虚函数,func3
为Derive
自己内部的虚函数。
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
问题一:上述多继承中,子类大小?
int main()
{
Derive d;
cout << "Derive:" << sizeof(d) << endl;
return 0;
}
结果如下:根据继承,至少我们能够知道,子类Derive d中除了有相关成员变量,还有对应的虚表指针。有一个问题是,子类继承多个父类,多个父类共用一个虚表,还是彼此有独立的虚表?
由上述结果,我们可以初步判断,多继承中,子类分别继承父类各自的虚表。接下来我们通过监视窗口来观察:
可以看到,1、父类在子类中有各自的虚表,因此子类实际存储了父类各自的虚表指针;2、对于重写的虚函数func1,在两个父类虚表中都存在,对于父类自己的虚函数func2,子类也将其继承下来,同样存储在父类的虚表中。
同样,这里也存在两个问题:
1、子类自己也有虚函数func3,而子类只有两个虚表,那么这个虚函数存储在哪个虚表中?
2、既然func1构成虚函数重写,在对应的父类虚函数表中都指向同一个(由Derive::func1
可知),为什么二者地址不同0x004a1230、0x004a133e
?
对问题一:子类自己的虚函数存储位置
解决方案:同之前类似,我们可以将子类中虚函数表打印出来观察。
typedef void(*VFPTR)();
void PrintVTable(VFPTR VFtable[])
{
cout << "虚表地址>" << VFtable << endl;
for (size_t i = 0; VFtable[i] != nullptr; ++i)
{
printf("VTable[%d],地址为:%p,对应函数为:", i, VFtable[i]);
VFtable[i]();
}
cout << endl;
}
1、打印Base1的虚表,PrintVTable((VFPTR*)(*(int*)(&d)));
,直接对d对象进行转换即可。
2、打印Base2的虚表,这个需要进行一定处理,我们要在d对象中找到Base2的起始位置,又或者说Base2的虚表指针,再根据虚表指针取地其虚表函数。
①方法一:(char*)&d+sizeof(Base1))
:我们可以对d对象取地址,再跳过整个Base1类,但需要注意指针访问权限,(char*)&d
,需要先将其强制类型转换为char*,这样指针±整数,单次访问的就是一个字节。
②方法二:除了上述方法,我们还可以使用切片思想Base2* ptr2 = &d;
int main()
{
Derive d;
cout << "Derive:" << sizeof(d) << endl;
//打印Base1虚表:
VFPTR* ptrBase1 = (VFPTR*)(*(int*)(&d));
PrintVTable(ptrBase1);
//打印Base2虚表:借助sizeof
VFPTR* ptrBase2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(ptrBase2);
//对于打印Base2虚表的另一种方法:切片
Base2* ptr2 = &d;
PrintVTable((VFPTR*)(*(int*)ptr2));
return 0;
}
对问题二:为什么指向子类同一个虚函数,但二者地址不同?
结论:
整体演示代码如下:
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
typedef void(*VFPTR)();
void PrintVTable(VFPTR VFtable[])
{
cout << "虚表地址>" << VFtable << endl;
for (size_t i = 0; VFtable[i] != nullptr; ++i)
{
printf("VTable[%d],地址为:%p,对应函数为:", i, VFtable[i]);
VFtable[i]();
}
cout << endl;
}
int main()
{
Derive d;
//打印Base1虚表:
VFPTR* ptrBase1 = (VFPTR*)(*(int*)(&d));
PrintVTable(ptrBase1);
//打印Base2虚表:借助sizeof
VFPTR* ptrBase2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(ptrBase2);
//对于打印Base2虚表的另一种方法:切片
Base2* ptrBase22 = &d;
PrintVTable((VFPTR*)(*(int*)ptrBase22));
d.func1();
Base1* ptr1 = &d;
ptr1->func1();
Base2* ptr2 = &d;
ptr2->func1();
return 0;
}
该问题我们需要结合一点汇编来理解: