write in front
所属专栏: C++学习
️博客主页:睿睿的博客主页
️代码仓库:VS2022_C语言仓库
您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如下面这两个例子,不同对象去完成同一件事情,结果是不一样的:
在继承中要构成多态还有两个条件:
先来说第一个,虚函数的构成就是在一个类成员函数(必须是一个类的成员函数才可以,普通函数加了会报错)前面加virtual
即可,但是这个virtual
和虚拟继承的virtual是没有关系的,一定不要搞混。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
这里一定一定不要和继承的隐藏搞混!!!
举个例子:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
这种情况下,Student
的BuyTicket
函数就对Person
的BuyTicket
函数进行了重写。
要形成多态,就只能通过指针或引用来调用,不然和之前的普通调用没什么区别:
多态调用:
class Person
{
public:
virtual void BuyTicket() const
{
cout << "买票-全价" << endl;
return 0;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() const {
cout << "买票-半价" << endl;
return 0;
}
};
void func(const Person& p)
{
p.BuyTicket();
}
int main()
{
func(Person());
func(Student());
}
结果:
class Person
{
public:
virtual void BuyTicket() const
{
cout << "买票-全价" << endl;
return 0;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() const {
cout << "买票-半价" << endl;
return 0;
}
};
void func(Person p)
{
p.BuyTicket();
}
int main()
{
func(Person());
func(Student());
}
结果:
在这里我们就会发现,多态调用时,不同的对象传过去,调用不同的函数。
我们知道,虚函数的重写的要求就是虚函数+三同,但是下面的情况有点特殊:
派生类的重写虚函数可以不加virtual
(最好加上)
协变,返回的值可以不同,但是要求返回值必须是父子关系指针和引用,并且父类只能返回父类指针,子类只能返回子类指针,并且必须同时返回指针或同时返回引用:
class A{};
class B: public A
{};
class Person
{
public:
virtual A* BuyTicket() const
{
cout << "买票-全价" << endl;
return 0;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket() const {
cout << "买票-半价" << endl;
return 0;
}
};
void func(Person p)
{
p.BuyTicket();
}
int main()
{
func(Person());
func(Student());
}
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
但是为什么要这样处理呢?
其实就是为了让析构函数构成重写。
但是为什么要对其进行重写呢?
我们来看看下面这个例子就懂了:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
~Student() {
cout << "~Student()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[10];
};
int main()
{
Person* a = new Person;
delete a;
Person* b = new Student;
delete b;
}
很显然,这里造成了内存泄漏,因为我们堆里面的Student
对象没有清除掉。我们来看看重写析构函数后的样子:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual ~Student() {
cout << "~Student()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[10];
};
int main()
{
Person* a = new Person;
delete a;
Person* b = new Student;
delete b;
}
运行结果:
这就是对析构函数重写的意义所在,当我们希望对一个对象进行多态调用,在有动态内存管理的情况下调用析构函数,如果不重写,就会造成内存泄漏。
在C++11标准中引入的 override 和 final 关键字,用于增强面向对象编程的语法和语义,帮助开发者在继承和多态性方面更加清晰地表达意图和管理代码。它们分别用于指示虚函数的重写和禁止派生类进一步继承或函数的重写
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错:
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
在C++98里面,我们将构造函数或析构函数放在private
成员里面,在继承时没有对private
的访问权,就无法调用父类的构造函数,就无法继承:
class A
{
A()
{}
public:
static A CreateObj()
{
return A();
}
};
class B:public A
{};
int main()
{
//B bb;
//想创建A对象如何创建?
A a = A::CreateObj();
}
在基类后面直接加final就无法继承了:
class A final
{};
class B:public A
{};
我们先来看看一道经典面试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b
成员,还多一个__vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析
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;
}
通过观察和测试,我们发现了以下几点问题:
nullptr
。 但是,虚函数表是存在哪里的呢?我们知道,虚函数存在代码段,通过下面这段代码可以验证,虚函数表存在常量区:
说了这么久,多态形成的原理到底是什么呢?我们以下面代码为例子:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
int _a = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 1;
};
void Func(Person& p)
{
// 符合多态,运行时到指向对象的虚函数表中找调用函数的地址
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
通过监视窗口可以看到,他们两个有不同的虚函数表指针,这是很关键的一点。
我们知道,构成多态有两个条件:
其实第一个条件,是为了让这个类里面的虚函数和普通函数不一样,虚函数会产生虚表指针,要调用虚函数时是通过这个虚表指针来找到虚表,在通过虚表调用虚函数;而普通函数不会,普通函数只是普通调用代码段里面的函数。而这个虚表指针就非常的重要,他就可以帮助我们形成多态。那么怎么帮助呢?第二个条件的价值就来了,我们通过父类的指针或引用指向子类,这个时候指针或引用要访问子类虚函数的时候,还是通过这个虚表指针来访问那个虚函数,这样才形成了多态。
说到这我们就不得不在说一下普通函数和虚函数调用时候的区别了:
eax
,而eax
存的就是虚函数表指针,所以多态调用不是在编译时确定。那么为什么必须是父类指针或引用呢?
子类赋值给父类对象切片,不会拷贝虚表指针。如果拷贝了虚表指针,那么父类对象虚表指针是子类虚表指针,一个父类对象调用子类的函数,就非常的奇怪。
其实上面的多态函数的调用和普通调用就很好的说明这一点。
class Person {
public:
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
virtual void Func2()
{
cout << "Person::Func2()" << endl;
}
//protected:
int _a = 0;
};
class Student : public Person {
public:
virtual void Func3()
{
//_b++;
cout << "Student::Func3()" << endl;
}
protected:
int _b = 1;
};
观察下图中的监视窗口中我们发现看不见func3。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。
那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数
typedef void(*Func_PTR)();
class Person {
public:
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
virtual void Func2()
{
cout << "Person::Func2()" << endl;
}
//protected:
int _a = 0;
};
class Student : public Person {
public:
virtual void Func3()
{
//_b++;
cout << "Student::Func3()" << endl;
}
protected:
int _b = 1;
};
void PrintVFT(Func_PTR* table)
{
for (int i = 0; table[i]; i++)
{
printf("[%d]->%p",i, table[i]);
Func_PTR p = table[i];
p();
}
printf("\n");
}
int main()
{
Student ss;
int ptr1 = *((int*)&ss);
PrintVFT((Func_PTR*)ptr1);
Person pp;
int ptr2 = *((int*)&pp);
PrintVFT((Func_PTR*)ptr2);
}
取出Person
对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
那么同样的问题在多继承里面会发生什么呢?
typedef void(*Func_PTR)();
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;
};
void PrintVFT(Func_PTR* table)
{
for (int i = 0; table[i]; i++)
{
printf("[%d]->%p",i, table[i]);
Func_PTR p = table[i];
p();
}
printf("\n");
}
int main()
{
Derive dd;
int ptr1 = *((int*)&dd);
//int ptr2= *((int*)(char*)&dd+sizeof(Base1));
Base2* pp = ⅆ
int ptr2= *((int*)pp);
PrintVFT((Func_PTR* )ptr1);
PrintVFT((Func_PTR* )ptr2);
}
我们会发现,派生类继承两个类,并且这两个类都有多态,那么他就会有两个虚函数表指针,如果在派生类里面多加一个虚函数,会加在第一个虚函数表里面。
并且我们会发现,对于两个父类在子类中同时构成多态的函数,子类可以重写,但是在重写之后会发现,这两个虚函数表里面重写的这个函数在代码段里面好像地址不统一?
其实原因很简单,我们通过以下代码来解释:
当用父类Base2指针来调用虚函数形成多态的时候,此时调用的是Derive的函数,要访问Derive的所有成员,就必须用Derive的this
指针,而此时如果直接通过Base2
指针调用func1
,this
指针传的就是Base2
类型的,所以要将其地址指向this
指针。这就是其指向其他位置的原因,为了修正其位置,看汇编就会知道,最后调用的函数地址和Base1一样。而Base1
就不用,因为this
指针指向的和Base1
指向的是同一个位置。
这里我们就要知道,内存里面不分类型,只分数据。
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!
专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!