多态
- 1. 前言
- 2. 多态的概念以及定义
- 3. 多态的实例调用情况
- 4. 构成多态的两个特例
- 5. 多态的底层原理分析(一)
- 6. 多态底层原理分析(二)
- 7. 多态中的两个关键字
- 8. 抽象类以及虚函数的几个结论
- 9. 总结以及拓展
1. 前言
继承和多态这两兄弟常常一起出现
继承是实现多态的前提!
本章重点:
注:如果你不知道什么是继承,或继承
的知识掌握不牢固,请先阅读下面文章:
C++继承深度剖析
2. 多态的概念以及定义
概念:
通俗来说,多态就是多种状态
父子对象完成相同任务会产生不同的结果
比如:
学生和普通人都去买门票
学生是半价,而普通人是全价
在继承中构成多态要有两个条件:
- 必须通过基类的指针或引用调用虚函数
- 被调用的函数必须是虚函数
并且子类的虚函数要被重写
现在的你可能有一万个问号
什么是虚函数?什么是重写?
没关系,我们一步一步讲!
关键字virtual加在成员函数前
这个成员函数就是虚函数!
虚函数的重写(也叫覆盖)
:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
上面的代码中,BuyTicket函数就被重写了!
概念讲完,下一步进行实战!
3. 多态的实例调用情况
构成多态的条件就两个,一定要熟记!
一定要熟记!一定要熟记!重要的事情说三遍
下面是多态的实例:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
p1->BuyTicket();
p2->BuyTicket();
return 0;
}
我们知道一个事实:
基类的指针或引用可以指向/引用
子类的对象,我们称为切片
依次打印:"买票-全家","买票-半价"
4. 构成多态的两个特例
特例一:
子类的虚函数不写virtual
依旧构成多态
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
};
Person* p1 = new Person;
Person* p2 = new Student;
p1->BuyTicket();
p2->BuyTicket();
这样写也是构成多态的!
特例二:
基类与派生类虚函数返回值类型不同
也可以构成多态(返回值必须满足某种条件)
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
父类的返回值要返回父类
子类的返回值要返回子类
注意事项1:
父类不写virtual,而子类的同名
函数写了virtual,这是不构成多态的!
class Person {
public:
void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
不构成多态!
注意事项2:
在继承体系中,父子类的同名
函数不构成重写就构成隐藏,不可能构成重载!
5. 多态的底层原理分析(一)
如果你单纯的认为Base类只有一个
整型变量占用空间的话,那你就上当啦!
事实上在32位机器下,这里的结果是8
在64位机器下,这里的结果是16!
这是因为它除了有一个变量外,还有
一个指针,此指针指向一个虚函数表
我们通过以下的代码来观察内存:
class A
{
public:
virtual void func1()
{
cout << "父类func1";
}
private:
int _a;
};
class B : public A
{
public:
virtual void func1()
{
cout << "子类func1";
}
private:
int _b;
};
int main()
{
A a;
B b;
return 0;
}
此指针叫虚表指针:vfptr,也就是
virtual function ptr
注:不管有没有继承体系或多态
只要有虚函数就有虚表!
6. 多态底层原理分析(二)
现在得出一个结论:有虚函数的
类对象中还存放了一个虚表指针!
通过下面的代码来观察内存情况
得出父子类虚表的关联:
class A
{
public:
virtual void func1()
cout << "父类func1";
virtual void func2()
cout << "父类func2";
private:
int _a;
};
class B : public A
{
public:
virtual void func1()
cout << "子类func1";
private:
int _b;
};
int main()
{
A a;
B b;
return 0;
}
请看下图观察情况:
结论:
拓展结论:同一个类的不同对象共用一个虚表
多态的原理深度剖析:
当一个函数A被重写时,它的父类虚表存放
父类函数A的地址,子类虚表存放的是子类
函数A的地址!
拓展结论:
父子类都只有A函数或无函数时
-
若父类写了虚函数A,而子类
甚至没有写函数A,此时子类对象中
存储的虚函数地址与父类相同 -
若父类甚至没有写函数A,而子类
直接写了虚函数A,则父类对象中没有
虚表,而子类对象中有虚表(存放A)
7. 多态中的两个关键字
final:
修饰虚函数,表示该虚函数不能被重写
override:
检查子类类虚函数是否重写了
基类虚函数如果没有重写编译报错
8. 抽象类以及虚函数的几个结论
抽象类概念:
抽象类的只需了解概念,实际中
使用到的场景很少
关于虚函数的几个小结论:
- 析构函数最好定义为虚函数
- 构造函数不能定义为虚函数
- 静态成员函数不能是虚函数
- 内联函数(inline)不能是虚函数
为什么说析构函数最好定义为虚函数?
请看下面的例子:
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数
//下面的delete对象调用析构函数,才能构成多态
//才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
9. 总结以及拓展
多态在校招的笔试面试中考察的
非常之多,很多面试官都喜欢在这
上面考察学生的掌握C++语法的程度
所以同学们请耐心学习!
拓展阅读:
多继承场景下的多态