前言
C
目录
1. 多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 一个接口,多种行为。
重写其实是一种接口继承
2 多态的定义及实现
2 .1 虚函数:
虚函数: 即被 virtual 修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
2 .2 虚函数的重写:
虚函数的重写(覆盖):
- 派生类中有一个跟基类完全相同的虚函数
- 即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同(三同)
- 称子类的虚函数(重写)了基类的虚函数,也叫(覆盖)
不符合重写,就构成隐藏。。。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:子类虚函数不加Virtual,依旧构成重写(实际最好加上)*/
2 .2.1 虚函数重写的两个例外:
- 协变—基类与派生类虚函数返回值类型不同:
派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
- [ ] 协变的返回值类型可以不同,要求必须是(父子关系)的指针和引用
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;}
};
2.析构函数的重写
- 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同
- 虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
2 .3 多态的两个条件(重点)
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2 .4 析构函数为啥写成虚函数
1.析构函数不构成多态的情况:
在这里,析构函数默认是隐藏关系
class Person {
public:
~ Person ()
{
cout << "~ Person( )" << endl;
}
};
class Student : public Person {
public:
~ Student ()
{
cout << "~ Student ( )" << endl;
}
};
2.析构函数构成多态:
class Person {
public:
virtual ~ Person ()
{
cout << "~ Person( )" << endl;
}
};
class Student : public Person {
public:
virtual ~ Student ()
{
cout << "~ Student ( )" << endl;
}
};
其析构函数最好定义为虚函数。见下面的例子:
- 指向谁,就调用谁析构。
3 新增的两个关键字
- C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
3.1 final的使用:
final:修饰虚函数,表示该虚函数不能再被重写。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
3.2 override :
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
重载、覆盖(重写)、隐藏(重定义)的对比
- 同名的成员变量也是隐藏的关系
4 抽象类
4.1 概念
- 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
- 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
5 多态的原理
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些
平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针
同一类型的对象共用一个虚表
不管是否完成重写,子类虚表和父类虚表都不是同一个
- 普通函数调用,编译链接时确定函数的地址,运行时直接调用。
- 多态调用,程序运行时去指向对象的虚表中找到函数的地址,进行调用。
总结:
多态的本质原理,当符合多态的两个条件时,
那么调用时,会到指向对象的虚表中找到对应的虚函数地址,进行调用
5.1 虚函数表存在那个区域
虚表存在哪个区域?
- 虚表应该是一个类型共用一个虚表
- 虚表应该存在一个长期存储的区域,是在常量区/代码区存着
6 inline函数可以是虚函数吗?
总结:
- C++在这里处理很灵活
- 只要是构成多态的时候,那么就会忽略掉内联的属性
- 既是保存了虚函数重写的多态特性,又保存了内联 - 这是不可能的
- 如果是多态属性的话那么内联的属性就丢了!!
- 因为要将函数地址放在虚表里面去。
7 静态成员函数不可以是虚函数
语法直接限制死了
- 静态成员函数不可能满足多态,不可能完成虚函数的重写,也不可能满足多态的运行时决议
- 因为静态成员函数没有this指针
8 构造函数可以是虚函数
答案是:不可以
- 虚函数表在编译阶段就生成的。
- 但虚函数表是在构造函数阶段才去初始化的。(调用构造函数就需要去找vptr,但此时vptr还没有初始化,对象内存空间都还没有)
- 调用构造函数后, 才能生成一个对象。 假设构造函数是虚函数, 虚函数存在于虚函数表中, 而去找虚函数表又需要虚函数表指针, 而虚函数表指针又存在于对象中, 这样就矛盾了: 都没有生成对象, 哪有什么虚函数表指针呢?
9 析构函数可以是虚函数
答案是:可以!并且必须是
- 因为基类的指针有可能指向父类对象,也可能指向子类对象
- 如果不是析构函数,就会有问题
代码举例:
class Person
{
public:
virtual ~Person()
{ cout<<"~Person()"<<endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* ptr1=new Person;
delete ptr1;
//如果析构函数不是虚函数,当基类指针指向父类时,就会报错
Person* ptr2=new Student;
delete ptr2;
}
尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦