一,正确调用析构函数
先看下面代码的执行结果
class Person1
{
public:
~Person1()
{
cout << "~Person()" << endl;
}
};
class Student1 : public Person1
{
public:
~Student1()
{
cout << "~Student()" << endl;
}
};
void main()
{
Person1* ptr1 = new Person1;
Person1* ptr2 = new Student1;
delete ptr1;
delete ptr2;
}
可以发现在析构子类对象的时候只调用了父类的析构函数,没有调用子类的,为了能正确调用子类的析构函数,需要对析构函数也实现成虚函数
class Person1
{
public:
virtual ~Person1()
{
cout << "~Person()" << endl;
}
};
class Student1 : public Person1
{
public:
virtual ~Student1()
{
cout << "~Student()" << endl;
}
};
void main()
{
Person1* ptr1 = new Person1;
Person1* ptr2 = new Student1;
delete ptr1;
delete ptr2;
}
二,接口继承
直接结合下列的代码和题目了解接口继承的细节,只有构成多态才能实现接口继承
class A1
{
public:
virtual void func(int val = 1) { std::cout << "A1->" << val << std::endl; }
virtual void test() { func(); }
//这里是this去调用func,this的类型是A1* A1是父类,是用父类的指针或引用去调用虚函数,构成多态
};
class B1 :public A1
{
public:
void func(int val = 0) { std::cout << "B1->" << val << std::endl; }
};
class B2 : public A1
{
public:
void func(int val = 0) { std::cout << "B2->" << val << std::endl; }
virtual void test() { func(); }
//这里的test的this是B2*,不是父类指针或引用,不构成多态
};
void main()
{
B1* p1 = new B1;
p1->test(); //打印B1->1
//p1是指针,指向test然后this调用func,this的类型是A1,是父类的指针或引用,构成多态,所以调用子类重写的func
//函数重写重写的是实现,继承的是接口,所以B2中的func继承了A1的func的缺省参数
B2* p2 = new B2;
p2->test();//打印B2->0
//p12是指针,指向test然后this调用func,this的类型是B2,不是父类的指针或引用,不构成多态,正常调用子类自己的func
}
三,虚函数表指针和虚表
虚函数表本质是一个虚函数指针数组,对象中存储的叫做虚函数表指针,这只是一个指针,占用四字节,这个指针指向虚函数表,虚函数表存储在常量区内,很多人经常混淆,认为虚函数表存在对象中,其实是错误的。
同种类型实例化出的对象共用同一张虚表,而且,如果子类实现了父类没有的虚函数,那么这个虚函数的地址将被放进第一个被继承的类当中,如下代码
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Derive : public Base1 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }//父类都没有func3,那么这个虚函数放第一个被继承的类的虚表里去
private:
int d = 3;
};
typedef void(*VFPTR) (); //声明一个函数指针,将void(*) ()声明为VFPTR
void PrintVFTable(VFPTR vTable[]) //函数指针数组
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i) //VS系列编译器存储虚表时,会在最后的位置放一个nullptr
{
printf(" [%d]:%p ->", i + 1, vTable[i]);
VFPTR f = vTable[i];
f(); //调用虚函数
}
cout << endl;
}
void main()
{
//同种类型实例化出的对象共用一张虚表
Base1 b1;
Base1 b2;
Derive d;
PrintVFTable((VFPTR*)(*(int*)&b1));
PrintVFTable((VFPTR*)(*(int*)&b2));
PrintVFTable((VFPTR*)(*(int*)&d));
}
四,多继承的虚函数表指针问题
先看下列代码和执行结果
//多继承的虚表
class base1
{
public:
virtual void func1() { cout << "base1::func1" << endl; }
virtual void func2() { cout << "base1::func1" << 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; }
};
void main()
{
derive d;
base1* ptr1 = &d;
base2* ptr2 = &d;
ptr1->func1();
ptr2->func2();
cout << endl;
PrintVFTable((VFPTR*)(*(int*)(ptr1)));
PrintVFTable((VFPTR*)(*(int*)(ptr2)));
}
根据打印结果我们发现,func1虚函数重写过后,都是derive的func1,但是打印func1的地址的时候,两个函数的地址却不一样,要想解释这种现象我们需要通过反汇编指令来解析,如下图
①ptr1->func1的汇编Call后面的eax存了一个jmp指令的地址,会跳到这个jmp指令,然后jmp再跳一次就是函数真正的地址,是正常调用
②ptr2的汇编Call后跳到一个jmp指令,然后跳到了另一个jmp,并且还对eax减了8,然后再jmp再跳到一个jmp,然后再jmp才是真正的函数的地址
所以问题来了,为什么执行ptr2的汇编指令时会跳三次jmp并且还要减去8呢,这个8代表什么呢?
先看下列对象模型图片
首先,jmp三次是为了对sub ecx进行包装,ecx存的是this指针,减去8是为了使this指针从base2的位置返回到base1也就是derive对象的起始位置,所以我们知道了这个8其实是base2的大小(这里base2的大小是8个字节,起始的虚表指针占四个字节,内置的int b2占四个字节),所以减8是为了修正ptr2的this指针位置,使其被修正到对象起始位置
关于多态的一些问题解答
①多态的条件之一是使用父类对象的指针或引用调用虚函数,那么为什么不能用对象调用呢?
class Person3
{
public:
virtual void BuyTicket() { cout << "Person::买票-全价" << endl; }
virtual void Func() { cout << "Person::买票-全价" << endl; }
};
class Student3 : public Person3
{
public:
virtual void BuyTicket() { cout << "Student::买票-半价" << endl; }
virtual void Func() { cout << "Student::买票-半价" << endl; }
};
void Func3(Person3& p)
{
p.BuyTicket();
}
Func参数是指针或引用时,子类赋值给父类,是通过切片完成的,父类类型的指针或引用指向子类中父类的那一部分,当参数是对象的时候,就需要把子类中父类的那一部分切出来拷贝一份然后再父类对象,其中也包括虚表的拷贝
但是如果拷贝了虚表,就乱套了,Func参数的父类对象中有子类的虚表,这是不合理的,所以切片时,虚表不能拷贝过去,应该让父类用自己的虚表,所以切片拷贝的时候只拷贝成员,不拷贝虚表,所以不能用对象做Func函数的参数,应该用指针或引用
②静态成员函数可以是虚函数吗?
不能。因为静态成员函数存在静态区不存在对象中,所以静态成员函数没有this指针,使用类型::成员函数的调用方法无法访问虚函数表,因为静态成员函数无法放进虚函数表
③多态中虚函数的重写为什么又叫做覆盖?
重写是语法层的概念:
重写是把父类的接口继承下来重写实现
覆盖是原理层的概念:
覆盖是子类重写虚函数之后,会先把父类的虚表拷贝一份给子类的虚表,然后把子类重写的虚函数覆盖掉原来父类的虚函数,这样调用虚函数时,父类去父类的虚函数表中找,子类去子类的虚函数表中找,就可以保证调用父类和子类的同名虚函数时执行的结果不同了