目录
一、多态的概念
二、多态的定义及实现
🌟多态的构成条件
🌟虚函数
🌟虚函数的重写
🌠小贴士:
🌟C++11 override 和 final
🌟重载、重写(覆盖)、重定义(隐藏)的对比
三、抽象类
🌟概念
🌟接口继承和实现继承
四、多态的原理
🌟虚函数表
🌟多态的原理
🌟动态绑定与静态绑定
🌠小贴士:
一、多态的概念
多种形态,去完成某个行为,当不同的对象去完成时会产生出不同的状态。
二、多态的定义及实现
🌟多态的构成条件
多态是在不同关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
在继承中构成多态的两个条件:(指向谁调用谁)
1、必须通过基类的指针或者引用调用虚函数;
2、被调用的函数必须是虚函数(对象都不行),且派生类必须对基类的虚函数进行重写;
🌟虚函数
即 被 virtual 修饰的类成员函数称为虚函数。
class A
{
public:virtual void test(){ cout << "A : test()" << endl;}
};
🌟虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)。
#include<iostream>using namespace std;class Person
{
public:virtual void BuyTicket(){cout << "买票--全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票--半价" << endl;}
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person p;Student s;Func(p);Func(s);return 0;
}
注意:
在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后,基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是改种写法不是很规范,不建议这样使用。
✨函数重写的两个例外:
<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;}
};//只有派生类Student的析构函数重写了Person的析构函数,
//下面的delete对象调用析构函数,才能构成多态
//才能保证p1和p2指向的对象正确的调用析构函数
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
🌠小贴士:
<1> 多态调用,看指向对象类型,指向谁调用谁的虚函数;
普通调用,看调用者的类型,调用调用者的函数;
<2> 重写是一种特殊的隐藏;
隐藏:不符合多态就是隐藏。
<3>对于普通对象,写不写成虚函数都不受影响:
class Person { public:virtual ~Person() { cout << "~Person()" << endl;} };class Student : public Person { public:virtual ~Student() {cout << "~Student()" << endl;} };int main() {Student s;//加不加 virtaul 普通对象都不受影响 //析构先调用子类析构,在调用父类析构//所以说在子类/派生类结束了以后会自动调用父类的析构//派生类里面有一个父类对象return 0; }
<4>为什么析构函数一定建议设计成虚函数?
只有派生类的析构函数重写了基类的析构函数,delete对象调用析构函数才能构成多态,才能保证指向的对象能正确的调用析构函数。
//加 virtual 变成多态,此时指向子类,调用子类;指向父类,调父类 //析构是先析构子类,再自动调用父类的析构 //如果不加虚函数,就是直接调用父类, // 这样子的话,子类的析构就没有调用到,就会产生内存泄露 class Person { public:virtual ~Person(){cout << "~Person()" << endl;} };class Student : public Person { public:virtual ~Student(){delete _ptr;cout << "~Student():" << _ptr << endl;} protected:int* _ptr = new int[10]; }; //内存泄漏 //没有调到派生类的析构函数( *_ptr 这个指针没有被释放)int main() {// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。Person* p1 = new Student;//指向子类调用子类的析构,最后默认调用父类的析构delete p1;Person* p2 = new Person;//指向父类,调用父类的析构,delete p2; //但是不会自动调用子类的析构,就会有可能参数子类的内存泄漏//因此析构函数一定建议设计成虚函数,在继承里面,尤其是基类的析构函数//因为只有当基类的析构函数设计成虚函数,派生类不管加不加virtual,派生类都构成了重写//构成了重写后,才能正常去选择调用子类/父类的析构,才能符合指向谁调用谁return 0; }
🌟C++11 override 和 final
C++对函数重写的要求比较严格,这两个关键字可以帮助用户检测是否重写。
<1> final:修饰虚函数,表示该虚函数不能再被重写。
class Person {
public:virtual ~Person() final //不想被完成重写,此时子类就不能被重写{cout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() //报错,不能完成重写{delete _ptr;cout << "~Student():" << _ptr << endl;}
protected:int* _ptr = new int[10];
};int main()
{Person* p1 = new Student;delete p1;Person* p2 = new Person;delete p2;return 0;
}
<2> override :检查派生类虚函数是否重写了基类的某个函数,如果没有重写编译报错。
class Person {
public:~Person()//没有完成重写{cout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() override //检查派生类是否完成重写,没有完成就报错{delete _ptr;cout << "~Student():" << _ptr << endl;}
protected:int* _ptr = new int[10];
};int main()
{Person* p1 = new Student;delete p1;Person* p2 = new Person;delete p2; return 0;
}
🌠小贴士:
<1> final可修饰类,修饰的类叫最终类,语法规定,这个类不能被继承,继承就会报错。
class A final {};class B : public A//报错 {};
<2>把基类的构造函数私有,子类就生不成对象:
class A { private:A(){} };class B : public A//报错 {};int main() {B bb;return 0; }
此时可以把基类的构造函数写成静态成员,就可以进行构造:
class A { public:static A CreateObj()//此时就可以用构造{return A();} private:A(){} };class B : public A {};int main() {//B bb;A::CreateObj();return 0; }
🌟重载、重写(覆盖)、重定义(隐藏)的对比
三、抽象类
🌟概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写。另外纯虚函数更体现出了接口继承。
在实践中,一个类型在现实没有实体对象,不想实例化出对象,设计成抽象类。
class Car
{
public:virtual void Drive() = 0;
};
//当父类是纯虚函数,派生类不想继承时,要重写虚函数
class Benz :public Car
{
public:virtual void Drive()//重写虚函数{cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive()//重写虚函数{cout << "BMW-操控" << endl;}
};
int main()
{Benz bz;//不想继承父类的纯虚函数时,在派生类中重写虚函数Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}
🌟接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四、多态的原理
🌟虚函数表
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;\
};int main()
{Base b;cout << sizeof(Base) << endl;return 0;
}
• 在x86的环境下,通过观察测试我们发现 b 的对象是 8bytes ,除了_b成员,还多一个 _vfptr 放在对象的前面(注意有些平台可能放在对象的最后面,这跟平台有关),对象中的这个 _vfptr 我们叫做虚函数表指针(v为virtual,f为 function)。一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到对象的最后面,这个跟平台有关)。
• 虚函数表 本质是一个函数指针数组。
• 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那派生类中这个表放了些什么呢?咱接着往下看:
🌟多态的原理
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
};void Func1(Base* p)
{//运行时绑定/动态绑定//运行时去虚表里面找到函数的地址,确认函数的地址,所以指向谁调用谁p->Func1();//有虚函数,放进虚表里面//普通的调用//编译时绑定/静态绑定 //即编译时,用函数名找,如果只有声明就链接的时候找,如果直接就有定义,就在编译的时候,用这个函数名,在符号表里面找这个函数的地址p->Func3();//没有虚函数,不放进虚表
}int main()
{Base b;Derive d;Func1(&b);Func1(&d);//Func1(new Base);//Func1(new Derive);//指向的是子类当中父类的那一部分return 0;
}
• 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
• 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
• 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。• 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
• 总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中;
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
🌠注意:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
🌟动态绑定与静态绑定
• 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的,不满足多态的函数调用时编译时就确认好的。
• 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载• 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态。
🌠小贴士:
<1>指定类域后,还能实现多态吗? 不能。
原因:编译器在编译时,指定类域,就不会识别成多态调用,就不会按照运行时绑定的方式去生成指令,此时就跟指向的对象没有关系;
<2>为什么虚函数要放到虚表?
运行的时候要去虚表里面找对应的虚函数,如果完成了重写就可以达到指向那个对象调用哪个对象;
<3>普通函数为什么不用放到虚表?
虚表时运行时才用的,普通函数是在编译的时候,通过函数名去确定地址,所以就支持了函数重载,按照函数名修饰规则,参数不同,修饰出来的函数名就不相同,就可以找到对应的地址;
函数名的地址有可能是在编译的时候找,也有可能是在链接的时候找,
在编译的时候,当前文件就用定义,编译好的时候当前文件就有这个函数的地址,编译就可以变成call的地址;
在链接的时候,只有声明,定义在其他.cpp,编译的时候时匹配的,再链接时才会拿修饰的函数名去其他文件的符号表里面找,找不到就会链接报错。
🌠小贴士:
<1> inline函数可以是虚函数吗?
可以,不过编译器就忽略了inline属性,这个函数就不再是Inline,因为虚函数要放到虚表中去。
<2> 静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用 类型::成员函数 的调用方法无法访问虚函数表,所以静态成员函数无法放进虚函数表。
<3> 构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
<4>析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。
<5>对象访问普通函数快还是虚函数快?
首先如果是普通对象,是一样快的。
如果是指针对象或者引用对象,则调用普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
<6>虚函数表是在什么阶段生成的?存在哪?
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
<7>什么是抽象类?抽象类的作用?
抽象类强制重写了虚函数,另外抽象类体现了接口继承关系。
如若对你有帮助,记得点赞、收藏、关注哦!
若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢^ ^ ~