文章目录
- 多态的概念及构成
- 虚函数重写
- override 和 final
- 重载,重写,重定义区别
- 抽象类(接口类)
- 多态的原理
- 虚函数表和函数地址
- 打印虚函数表
- 多继承中的虚函数表
多态的概念及构成
概念:
通俗的来讲,就是多种形态,同一个函数,不同对象去调用产生不同的状态
构成条件:
1.必须通过基类的指针或者引用调用虚函数
2.调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:被virtual修饰的函数
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人 全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生 半价" << endl;
}
};
void Function(Person& per)
{
per.BuyTicket();
}
int main()
{
Person p;
Student s;
Function(p);
Function(s);
return 0;
}
相同的函数,传入不同的对象,产生不同结果这就是多态
虚函数重写
又称覆盖, 派生类中有一个函数与基类完全相同的虚函数(函数名,返回值,参数列表完全相同),称子类的虚函数重写了基类的虚函数
若派生类中一函数和基类中一虚函数完全相同,那么其可以不加virtual关键字依然构成重写,但是这样不是很规范,不推荐使用
虚函数重写的两个例外
1.协变(基类和派生类返回值不同):派生类称谢基类虚函数时,返回值可以不同。但是基类虚函数返回的必须是基类对象的指针或引用,派生类虚函数返回的必须是派生类对象的指针或引用。
class Person
{
public:
virtual Person& BuyTicket()
{
cout << "成人 全价" << endl;
return *this;
}
};
class Student : public Person
{
public:
virtual Student& BuyTicket()
{
cout << "学生 半价" << endl;
return *this;
}
};
2.析构函数重写:如果基类析构函数为虚函数,那么派生类析构函数只要定义,无论是否添加virtual关键字都和基类析构函数构成重写。因为编译器会对析构函数的函数名进行处理,编译后析构函数的名称统一为destructor
int main()
{
Person* p = new Person;
Person* s = new Student;
delete p;
delete s;
return 0;
}
以上是析构函数没有重写的情况,当使用父类指针来接收子类对象时,在释放的时候编译器并不知道该指针指向对象是父类还是子类。**仅能根据指针类型进行空间释放。这样子类的数据并未被清除干净,就会产生内存泄漏。**所以在继承体系中,我们最好将析构函数定义为虚函数
class Person
{
public:
virtual ~Person()
{
cout << "~Person" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person* p = new Person;
Person* s = new Student;
delete p;
delete s;
return 0;
}
为什么析构函数重写后就可以无视指针类型正确释放对象空间呢?多态的原理会在后文进行讲解
override 和 final
final关键字:修饰虚函数,表示该虚函数不能被继承
在C++98中,创建一个不能被继承的类需要按照以下写法:
将基类的构造函数定义为私有,这样派生类就无法调用到基类的构造函数也就无法构造派生类对象。但是这样基类自己也无法生成对象,因为构造函数隐藏,我们需要一个对象来调用构造函数,但是我们无法生成对象。这时候就可以定义一个全局的类函数来生成并返回对象,完成对象构造,并且返回对象
class Person
{
public:
static Person Construct(int a)
{
return Person(a);
}
private:
Person(int a = 1)
:_a(a)
{}
protected:
int _a;
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person a = Person::Construct(10);
return 0;
}
显然这样的方法有点麻烦,所以在C++11中添加了final这个关键字
在C++11中,类或者函数添加了final代表其无法被继承
class Person final
{
public:
static Person* Construct(int a)
{
return new Person;
}
virtual ~Person() final
{
cout << "~Person" << endl;
}
protect:
int _a;
}
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student" << endl;
}
};
override关键字(写在派生类函数后面)
作用:检查派生类的虚函数是否重写了基类的虚函数,若基类的虚函数没有写,则报错
class Person
{
public:
protected:
int _a;
};
class Student : public Person
{
public:
virtual ~Student() override
{
cout << "~Student" << endl;
}
};
int main()
{
return 0;
}
重载,重写,重定义区别
重载:
1.在同一作用域
2.函数名相同,参数不同
重写(覆盖):
1.一个在基类作用域,一个在子类作用域
2.两个函数都为虚函数
3.返回值相同,函数名相同,参数相同
(协同,和析构函数例外)
重定义(隐藏):
1.一个在基类作用域,一个在子类作用域
2.两个函数名相同,不构成重写就是重定义
抽象类(接口类)
概念:包含纯虚函数的类叫做抽象类(也叫接口类), 抽象类不能实例化对象,并且其派生类不对父类虚函数进行重写也无法实例化对象。纯虚函数的存在让派生类强制定义接口必须重写,另外纯虚函数更体现了接口继承
class Person
{
public:
virtual void func() = 0
{
cout << "Person::func()" << endl;
}
protected:
int _a;
};
class Student : public Person
{
public:
};
int main()
{
Person* p = nullptr;
p->func();
return 0;
}
我们在基类加入一个函数func()其是一个纯虚函数。创建一个Person的派生类,其并没有重写基类的纯虚函数。可以看到程序可以通过编译,但是运行时会报错
编译时(成功)
运行程序(程序崩溃)
因为抽象类无法创建实例化对象,所以在抽象类中定义函数毫无意义,只需要声明就可以了
接口继承
class person
{
public:
virtual person& buyticket()
{
cout << "成人 全价" << endl;
return *this;
}
virtual void func()
{
cout << "person::func()" << endl;
}
protected:
int _age = 18;
int _id = 0;
};
class student : public person
{
public:
virtual student& buyticket()
{
cout << "学生 半价" << endl;
return *this;
}
private:
virtual void func()
{
cout << "student::func()" << endl;
}
int _stu_id = 1;
};
void function(person& per)
{
per.buyticket();
per.func();
}
int main()
{
person peter;
student ken;
function(peter);
function(ken);
return 0;
}
运行程序,我们发现即便派生类的func函数是私有的,我们呢还是可以通过多态调到。这说明访问限定符并非绝对安全。因为虚函数将自己的地址放在虚表中了,我们可以通过虚表找到此函数进行调用。
派生类的重写函数的访问限定跟从基类,因为派生类的虚表就是基类的拷贝,并将其重写函数的地址对原地址进行覆盖,这就是接口继承
但并非所有情况都会去使用虚函数表查找函数地址,只有其满足多态的条件(1.必须通过基类的指针或者引用调用虚函数 2.调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写)才会使用虚表查找函数地址
int main()
{
person peter;
student ken;
peter.buyticket();
function(peter);
function(ken);
return 0;
}
像这样一串代码,我们将func()设置为普通函数
我们可以看到虽然buyticket是虚函数,但是编译器并没有通过虚函数表进行查找,让我们再看一看function函数
可以看到普通函数直接就可以找到其地址,但是虚函数的地址为rax,这说明在编译阶段编译器并不知道应该掉哪个地址的函数,于是其去虚函数表中查找,当运行时根据传入对象的数据来进行填充地址
多态的原理
虚函数表
#include <iostream>
using namespace std;
class Person
{
virtual void func()
{
cout << "Person::func()" << endl;
}
private:
int _a;
};
class Student
{
virtual void func()
{
cout << "Student::func()" << endl;
}
};
int main()
{
int a = 0;
int* pa = &a;
cout << sizeof(pa) << endl;
cout << sizeof(Person) << endl;
return 0;
}
[clx@VM-20-6-centos polymorphic_c++]$ make
g++ -o test test.cpp -std=c++11
[clx@VM-20-6-centos polymorphic_c++]$ ./test
8
16
可以看到具有虚函数的类Person的大小并非和普通类相同,其还包含了一个指针,这个指针就指向我们的虚函数表
为了便于观察我们多增加几个成员变量和函数,在vs监视窗口中看看这个类中究竟包含了什么
#include <iostream>
using namespace std;
class person
{
public:
virtual person& buyticket()
{
cout << "成人 全价" << endl;
return *this;
}
virtual void func()
{
cout << "person::func()" << endl;
}
protected:
int _age = 18;
int _id = 0;
};
class student : public person
{
public:
virtual student& buyticket()
{
cout << "学生 半价" << endl;
return *this;
}
virtual void func()
{
cout << "student::func()" << endl;
}
private:
int _stu_id = 1;
};
void function(person& per)
{
per.buyticket();
}
int main()
{
person peter;
student ken;
function(peter);
function(ken);
return 0;
}
_vfptr:就是我们所说的虚函数表指针 virtual function pointer
我们还可以发现,派生类的虚函数表指针和基类的虚函数表指针不同
我们可以发现虚函数表是一个函数指针数组,里面存储了基类和派生类各自的虚函数的地址。我们创建的对象就是根据这些地址来找到对应的函数的
那么为什么动态多态只能传递父类的指针或者是引用呢
int main()
{
person peter;
student ken;
person& s_ken = ken;
person* p_ken = &ken;
person person_ken = ken;
return 0;
}
通过小实验我们发现,当我们使用**基类的指针或者引用来对派生类对象进行切片时,并不会改变对象的虚函数表指针。但是如果我们重新创建基类对象,虚函数表指针就会变成基类的虚函数表指针。**而这个指针决定着调用那一组函数
void function(person per)
{
per.buyticket();
}
这样我们就能解释为什么function处只能使用基类的指针或者引用了。若直接为基类对象的话,形参为实参的切片的拷贝,那么形参实例化一定为person类对象,那么其的虚函数表指针一定是person类的,那么不管传基类还是派生类对象都只能调用基类的函数组,就无法完成多态
虚函数表和函数地址
虚函数表中的地址和真正的函数地址可能是不同的,如果一个派生类有自己独有的虚函数,并没有构成重写,那么它也会被放在它的虚函数表中
class student : public person
{
public:
virtual student& buyticket()
{
cout << "学生 半价" << endl;
return *this;
}
virtual void func()
{
cout << "student::func()" << endl;
}
virtual void funb()
{
cout << "student::funb()" << endl;
}
private:
int _str_id = 3;
};
int main()
{
person peter;
student ken;
return 0;
}
让子类增加一个虚函数funb,可以看到监视窗口中并没有funb的地址,这是因为vs的监视窗口是被处理过的。我们复制虚函数表的指针,到内存窗口中看看
可以看到第三行好像就是funb的地址,说明funb函数是有存到虚函数表中的,我们再看看内存
我们发现函数的地址和虚函数表中存储的地址好像不同,这是因为虚函数表中存储的地址也是一个指令,这个指令会跳转到真正的函数地址。
打印虚函数表
#include <iostream>
using namespace std;
typedef void(*VF_PTR)();
class Base{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int _a = 0;
};
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 = 1;
};
void Print_vftable(VF_PTR* _vfptr)
{
for (int i = 0; *(_vfptr + i) != nullptr; i++)
{
printf("vft[%d]: %p\n", i, *(_vfptr + i));
}
}
void Print_vftable(Base& b)
{
VF_PTR* ptr = (VF_PTR*)(*((void**)&b));
Print_vftable(ptr);
cout << endl << endl;
}
int main()
{
Base b;
Derive d;
Print_vftable(b);
Print_vftable(d);
return 0;
}
调出监视窗口发现,重写函数func1地址被覆盖,继承函数func2地址在虚表中地址和基类相同。监视窗口中没有未被重写的派生类函数,这说明监视窗口是被处理过的。
我们甚至还可以通过函数地址直接调用函数
void Print_vftable(VF_PTR* _vfptr)
{
for (int i = 0; *(_vfptr + i) != nullptr; i++)
{
printf("vft[%d]: %p->", i, *(_vfptr + i));
VF_PTR f = *(_vfptr + i);
f();
}
}
多继承中的虚函数表
#include <iostream>
using namespace std;
typedef void(*VF_PTR)();
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 d1 = 3;
};
int main()
{
Derive d;
return 0;
}
可以看到派生类继承了两份虚函数表, 我们再通过内存窗口看看
在上述代码中,我们的Derive对fun1()进行了重写,它本应对两个基类的虚表都完成覆盖,但是我们发现,两个虚表中func1的函数地址并不相同
int main()
{
Derive d;
Base1* pd1 = &d;
Base2* pd2 = &d;
pd1->func1();
pd2->func1();
}
使用多态,将d的地址分别复制给pd1和pd2,并分别调用func1函数
可以看到两个函数最终都跳到了0x00052870(&Derive::func1)但是将pd2在调用过程中多跳了两次。
我们发现这两个指令对ecx进行了操作,而在前面的学习我们知道ecx是和this指针相关。其实这两个jmp就是为了修正this指针用的。因为调用成员函数编译器会自动传递this指针,当将d按Base1切片时,pd1的this指针任然指向&d。但是d按base2切片时,pd2的this指针其实在&(d + sizeof(Base1))的地方,而这个偏移量刚好也就是八个字节,所以在调用虚函数之前,编译器还会调用指令帮助我们校准this指针。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)