博客主页:@披星戴月的贾维斯
欢迎关注:点赞收藏留言
系列专栏: C/C++专栏
那些看似波澜不惊的日复一日,一定会在某一天让你看见坚持的意义!-- 算法导论
一起加油,去追寻、去成为更好的自己!
提示:以下是本篇文章正文内容,下面案例可供参考
C++的三大特性就是继承,封装,多态,上一篇博客我们已经学习了C++继承,这篇文章让我们一起走进C++多态的学习。多态、多态、是多种状态的意思吗?多态又是靠什么实现的呢?---- 详情请看这篇博客
1.1多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
打个比方:买动车票的时候,会有学生票、成人票、军人优先通道之类的,就是说,对于买票这件事,不同的人去买,会有不同的效果,多态就是实现不同的对象去完成同一件事时会有不同的效果。
1.2多态的定义和构成条件
多态的定义:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
继承中构成多态的两个条件:
2.1虚函数的概念以及为什么要引入虚函数
虚函数的概念:虚函数即被virtual修饰的类成员函数称为虚函数。
虚函数定义:语法形式
class X{
……
virtual 返回类型 函数名(函数参数表); //虚函数(成员)声明
……
}
虚函数的引入: 赋值相容性
由赋值相容性,通过基类指针、基类引用,都只能访问到派生类 从基类中继承到的成员(基类子对象中的成员),不能访问到派生类中定义的成员。
所以,我们如何通过基类指针/引用访问派生类中定义(增加)的成员?
答案:通过基类指针(或基类引用) 可以访问到派生类中 ‘重’定义 的成员。
所以我们得出虚函数的意义:
基类指针(或引用)指向 派生类的对象时,通过该指针(或引用)访问派生类中的虚函数,将调用该指针(或引用)实际所指对象的函数成员!
2.2虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
代码示例:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
虚函数重写的两个例外:
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;}
};
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;
}
2.3虚函数的特性
有 3种类型的函数成员不能定义为虚函数。
2.4 C++11 override 和 final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
class D1 :public D {
public:
void g1(int x) final { cout << x; } //正确,不允许D1的派生类覆盖g1
void f(int y) final {cout<
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
}
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
定义纯虚函数的语法格式:
class X {
virtual ret_type func_name (params) = 0;
}
#include
using namespace std;
class Figure{
protected:
double x,y;
public:
void set(double i,double j){ x=i; y=j; }
virtual void area()=0; //纯虚函数
};
class Triangle:public Figure{
public:
void area(){cout<<"三角形 面积:"<area(); //L4: 调用派生类的虚函数area()
Figure &rF=t;
rF.set(20,20);
rF.area(); //L5
}
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.1虚函数表
让我们一起分析一下下面这个题目:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
问:sizeof(Base)是多少?
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__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;
};
int main()
{
Base b;
Derive d;
return 0;
}
多态调用和普通调用的区别:
父类赋值给子类对象也可以切片,但是为什么实现不了多态?
原因:对象无法实现多态,因为实现多态需要拷贝虚表,但是对象切片时,子类不会拷贝父类的虚表。
子类的虚函数表存的是虚函数的地址,是为了实现多态;
虚基表存的是偏移量,是为了解决数据冗余和二义性。
本文和大家总结了C++多态的几个要点,从多态的概念、虚函数、抽象类和虚函数实现的原理四个方面和大家讲解多态这个要点,希望大家读后能够有所收获!