【C++】面向对象---多态(万字详解)
🔥🔥 欢迎来到小林的博客!!
🛰️博客主页:✈️小林爱敲代码
🛰️文章专栏:✈️小林的C++之路
🛰️欢迎关注:👍点赞🙌收藏✍️留言
今天给大家讲解多态,多态是面向对象的一个重要内容。也非常的抽象,所以今天尽我所能为大家分享自己对C++中多态的一些理解。
每日一句: 世界上只有想不通的人,没有走不通的路。
大纲:
目录
- 💖1. 多态的概念
- 💖2. 多态的定义及实现
- 🌺2.2 多态的构成条件
- 🌺2.3 虚函数
- 🌺2.4 虚函数的重写
- 🌺2.5 协变
- 🌺2.6析构函数的重写
- 🌺2.7 重载,重写(覆盖),重定义(隐藏)之间的区别
- 💖3. override 和 final(c++11)
- 💖4.抽象类
- 💖 5.多态的原理
- 💖 6.单继承和多继承的虚函数表
- 🌺 6.2 打印虚函数表
- 🌺 6.3 单继承的虚函数表
- 🌺 6.4 多继承的虚函数表
- 💖 7.多态面试问答题
- 总结🥳:
💖1. 多态的概念
多态的意思就是多种形态,简而言之就是 : 不同的事物做同一种行为,产生了不同的结果。
打个比方,学生和普通人买票,学生优惠7折,而普通人没有优惠。这类现象就符合多态,不同的事物(普通人,学生)做同一种行为(买票)产生了不同的结果(学生七折,普通人全款)。
而普通人和学生之间还有另外一种关系,那就是继承关系。因为学生也是人,所以构成多态的前提是不同的事物之间构成继承关系。
💖2. 多态的定义及实现
🌺2.2 多态的构成条件
想要知道多态如何定义,那么我们必须知道多态的定义条件。构成多态的两个条件:
1.必须通过基类的指针或引用调用虚函数。
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
🌺2.3 虚函数
虚函数:被virtual 修饰的函数即为虚函数。
class Person
{
public:
virtual void BuyTicket()//被virtual修饰,是虚函数
{
}
};
🌺2.4 虚函数的重写
虚函数的重写: 派生类必须有一个和基类一样(三同)的虚函数,才能构成重写。构成重写的条件:
1.派生类被重写的函数也得是虚函数(虽然不是也可以,因为会直接继承父类的虚函数)。
2.派生类被重写的函数和基类的虚函数一样 (函数名,返回值,参数都相同,协变除外)。
下面是一个虚函数构成重写的案例:
class Person
{
public:
virtual void BuyTicket()
{
cout << "成年人买票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生买票" << endl;
}
};
以上代码构成以下关系:
基类 : Person
派生类 : Student
基类的BuyTicket函数被virtual修饰,所以是虚函数。
派生类有一个和基类虚函数一模一样的虚函数。
所以派生类BuyTicket构成重写(覆盖)。
🌺2.5 协变
上面说过,被重写的函数必须与其基类对应的虚函数保持三同(返回值,函数名,参数),而协变是个例外,协变支持返回值是父子类的指针或引用。
代码案例如下:
class A{};
class B:public A
{};
class Person
{
public:
virtual A* BuyTicket()
{
new A();
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
new B();
}
};
返回值是父子类的指针或引用(也就是协变),一样会重写。
🌺2.6析构函数的重写
当我们在通过父类指针接收一个子类对象时,并期望释放掉这个对象。那么我们必须要让子类重写析构函数。也就是让析构函数变成虚函数,析构函数变成虚函数之后。子类会自动重写析构,这是因为在编译时析构函数的函数名会被统一处理为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;
}
};
void a(Person* p)
{
delete p;
}
int main()
{
Person* p = new Person();
Student* s = new Student();
a(s);
return 0;
}
我们这个代码是没有没有重写析构函数的,因为析构函数不是虚函数,我们看看结果。
我们会发现,问题很严!因为我传过去的是一个Student,也就是基类对象。但是我们用父类指针接收,那么 指针pp的使用范围 就是Person的范围。所以无法调用子类的析构函数,只能调用自己的析构函数。也就是说!释放不彻底,因为传过去的对象是s对象的指针,而delete它时,它却只调用了父类的析构函数,没有调用自己本身的析构函数,如果此时s对象有动态开辟的空间,那么就造成了内存泄露,这是很严重的。这是因为指针是Person类型的,所以只能访问Person的那一部分。想要解决这个问题,我们就需要重写析构函数。以至于传子类对象指针,父类指针接收也能调到子类的析构函数。
正确的写法:
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;
}
int _a;
};
void a(Person* pp)
{
delete pp;
}
int main()
{
Person* p = new Person();
Student* s = new Student();
a(s);
return 0;
}
这时候我们可以看到Student的析构函数也被调用了,这就意味着s对象被真正析构。所以析构函数还是很有必要被重写的。
🌺2.7 重载,重写(覆盖),重定义(隐藏)之间的区别
一张图概括
💖3. override 和 final(c++11)
override 和 final 在c++11才被引用,2个关键字的作用也很简单。
final:修饰虚函数,表示虚函数不能被重写。
override:检查派生类是否重写了虚函数,如果没重写,会报错。
final的使用:
override的使用:
💖4.抽象类
在虚函数的后面加上一个 = 0,这个函数就是纯虚,这就代表这是一个抽象类,也叫接口类,抽象类不能被实例化,派生类继承后也不能实例化出对象。除非重写其基类的纯虚函数。
代码样例:
class Person
{
public:
virtual void Eat() = 0
{
}
};
class Student : public Person
{
public:
};
int main()
{
Person p;
Student s;
return 0;
}
如果想使用,我们必须重写纯虚函数。
但是p依然不能实例化,想要p对象,我们可以通过指针或者引用的方式。
class Person
{
public:
virtual void Eat() = 0
{
}
};
class Student : public Person
{
public:
virtual void Eat()
{
cout << "吃饭" << endl;
}
};
int main()
{
Student s;
Person& p = s;
p.Eat();
return 0;
}
这种方法已经构成了多态,因为s通过了基类的指针调用其纯虚函数。
💖 5.多态的原理
那么多态是怎么实现的呢?我们先来监视一下,非多态时,子类对象和父类对象。
class Person
{
public:
void BuyTicket()
{
cout << "成年人买票" << endl;
}
int _p;
};
class Student : public Person
{
public:
void BuyTicket()
{
cout << "学生买票" << endl;
}
int _s;
};
int main()
{
Student s;
Person p;
return 0;
}
这是父类对象
这是子类对象
接下来我们看看实现多态时的样子。
class Person
{
public:
virtual void BuyTicket()
{
cout << "成年人买票" << endl;
}
int _p;
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生买票" << endl;
}
int _s;
};
int main()
{
Student s;
Person p;
return 0;
}
父类对象:
子类对象:
我们可以很清楚的发现,构成多态后,对象里面会多一个__vfptr的参数。而这个参数是虚函数表指针(简称虚表指针),它指向一个数组,数组的每个元素是一个函数指针。而这个数组,叫做虚函数表。而虚函数表里面存的就是虚函数的地址。当子类进行重写的时候,就会去虚函数表里面把存储的父类虚函数的地址覆盖成自己的虚函数地址。所以进行切片时,虚函数表里的虚函数地址还是自己的。调用虚函数时,去自己的虚函数表里面找到对应的虚函数。
所以多态的实现原理,简单来说就是以下几个步骤:
-
看父类有没有虚函数,如果有虚函数,则会在父类生成一个虚函数表。
-
子类继承父类时,会把父类的虚函数表也继承下来。
-
随后子类查找有没有符合重写条件的函数(三同,且是虚函数),符合重写条件则到继承的虚函数表里,覆盖掉父类的虚函数表。
此时如果构成多态,就会进Student的虚函数表里面找对应的虚函数调用,因为父类虚函数的地址被替换了。
💖 6.单继承和多继承的虚函数表
🌺 6.2 打印虚函数表
以下这段代码可以直接打印虚函数表,其原理取对象的地址,随后强制转换成一个指针。因为指针在32平台是4字节,在64平台是8字节。所以把对象强制转换成指针类型,访问的第一个元素就是一个指针的大小。因为虚表指针就是在对象的最前面4个或8个字节。然后强制转换成函数指针。
class Person
{
public:
virtual void fun1()
{
cout << "Person::fun1()" << endl;
}
virtual void fun2()
{
cout << "Person::fun2()" << endl;
}
int _p;
};
class Student : public Person
{
public:
virtual void fun1()
{
cout << "Student::fun1()" << endl;
}
virtual void fun2()
{
cout << "Student::fun2()" << endl;
}
virtual void fun3()
{
cout << "Student::fun3()" << endl;
}
virtual void fun4()
{
cout << "Student::fun4()" << endl;
}
int _s;
};
void a(Person& p)
{
p.fun1();
}
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl << endl;
}
int main()
{
Person p;
Student s;
VFPTR * vTableb = (VFPTR*)(*(void**)&p);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(void**)&s);
PrintVTable(vTabled);
return 0;
}
我们看看打印结果
这样我们就可以看到虚函数表的打印结果了。
🌺 6.3 单继承的虚函数表
上面说过的情况就是单继承时的情况,子类继承父类时会继承它的虚表。随后在虚表里面覆盖重写的函数。那么如果此时子类自己的函数也是虚函数,那么也会添加至虚函数表中。
两个类的关系如下所示
class Person
{
public:
virtual void fun1()
{
cout << "Person::fun1()" << endl;
}
virtual void fun2()
{
cout << "Person::fun2()" << endl;
}
int _p;
};
class Student : public Person
{
public:
virtual void fun1()
{
cout << "Student::fun1()" << endl;
}
virtual void fun2()
{
cout << "Student::fun2()" << endl;
}
virtual void fun3()
{
cout << "Student::fun3()" << endl;
}
virtual void fun4()
{
cout << "Student::fun4()" << endl;
}
int _s;
};
我们可以发现,子类的fun1和fun2与父类构成多态。可是fun3和fun4并没有构成多态,但是它们依然会被添加进子类的虚函数表。
所以虚函数表也会添加自身的虚函数。
🌺 6.4 多继承的虚函数表
那么如果是多继承呢?
以下代码实现了多继承,me继承了Base1和Base2。因此,Base1的虚函数表在m的前4/8个字节的位置。但是Base2的虚函数表可不在后面。所以要想知道Base2的虚函数表位置。我们需要m的地址在原有的基础上加一个Base1大小,这样就到达了Base2对象的首地址,再取前4/8个字节就是Base2的虚函数表。
class Base1
{
public:
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
int _p;
};
class Base2
{
public:
virtual void fun3()
{
cout << "Base2::fun3()" << endl;
}
virtual void fun4()
{
cout << "Base2::fun4()" << endl;
}
};
class me :public Base1,public Base2
{
virtual void fun1()
{
cout << "me::fun1()" << endl;
}
virtual void fun2()
{
cout << "me::fun2()" << endl;
}
virtual void fun5()
{
cout << "me::fun5()" << endl;
}
virtual void fun6()
{
cout << "me::fun6()" << endl;
}
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i+1, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl << endl;
}
int main()
{
me m;
VFPTR * vTableb = (VFPTR*)(*(void**)&m);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(void**)((char*)&m+sizeof(Base1)));
PrintVTable(vTabled);
return 0;
}
我们看看代码结果:
我们可以看到,当有一个类继承了多个类时,那么会产生多张虚函数表。而自己的虚函数(非重写) 将会被默认放在第一张函数表中。
💖 7.多态面试问答题
-
什么是多态?
2答:不同的事物做同一行为产生不同的结果。 -
什么是重载、重写(覆盖)、重定义(隐藏)?
答:重载要在同一作用域下,且函数名相同,但参数的顺序,个数,类型不同。
重写是当基类和派生类有一模一样的虚函数时,子类虚函数表中的父类虚函数地址会被覆盖。
重定义,从父类继承下来,且没有重写的就是重定义,重定义函数名,参数相同。 -
多态的实现原理?
答:父类的所有虚函数都会存在虚函数表中,而虚函数表存储在常量区。当子类继承了父类时,也会继承父类的虚函数表,如果此时子类又符合重写要求的函数。则会去自己的虚函数表中替换掉父类的虚函数地址,换成自己的虚函数地址。 -
inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。 -
静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 -
构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 -
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数,否则当子类和父类构成多态时。delete释放对象可能会导致内存泄漏,具体上面有讲解。 -
对象访问普通函数快还是虚函数更快?
答:构成多态,普通函数快。不构成多态,一样快。 -
虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。 -
什么是抽象类?抽象类的作用?
答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
总结🥳:
💦💦如果有写的有什么不好的地方,希望大家指证出来,我会不断的改正自己的错误。💯💯如果感觉写的还可以,可以点赞三连一波哦~🍸🍸后续会持续为大家更新C/C++,数据结构,Linux相关的知识
🔥🔥你们的支持是我最大的动力,希望在往后的日子里,我们大家一起进步!!!🔥🔥