前言
多态是基于继承而实现的一种方式, 我们可以通过实现不同对象调用相同函数进行不同事情的发生.
就像下图一样
这里和函数模板实现重载也有些相似都是使用同样的函数实现不同的功能但是,我们的多态可以一次实现多个函数的实现。在很多方面上都是个不错的选择。
下面让我们来讲解一下多态的实现及原理等。
多态的简单介绍
概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
多态的定义和实现
我们的多态是基于继承所实现的,我们用子类来继承父类的虚函数 并将其进行重写 操作。然后我们使用父类的指针或者引用来实现多态的实现。
上面一句话我们有两个疑点虚函数 重写操作 这些都是什么及如何操作,
虚函数
我们只需要将函数类中的函数前加上virtual 就可以实现我们的虚函数了。
如下代码:
class Person
{
public:
virtual void Buy()
{
cout << "原本买票--全价" << endl;
}
};
虚函数重写
虚函数的作用就是构成重写,当基类中的虚函数和子类中的函数的返回值,参数,函数名都相同的时候我们的就可以实现重写。
class Person
{
public:
virtual void Buy()
{
cout << "原本买票--全价" << endl;
}
};
class Student:public Person
{
public:
virtual void Buy()
{
cout << "学生买票--半价"<<endl;
}
};
上图中我们的Buy函数就实现了重写。
我们重写的函数就将不再继承父类函数的函数体,而只继承函数的声明。
证明如下图:
class T1
{
public:
virtual void test(int pos = 1)
{
cout <<"T1:" << pos << endl;
}
};
class T2: public T1
{
public:
virtual void test(int pos = 2)
{
cout <<"T2:" << pos << endl;
}
};
void Test(T1& t)
{
t.test();
}
int main()
{
T1 t1;
T2 t2;
Test(t1);
Test(t2);
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;
}
};
析构函数的重写(基类于派生类函数名称不同)
如果基类的析构函数为虚函数,此时派生类析构函数只需要定义无论是否加上virtual关键字,派生类的析构函数都与基类的析构函数构成重写,即使基类于派生类的析构名字不同。但是我们这里其实可以理解成我们的编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor 。
这样我们的多态的实现其实就可以实现了。
我们来看一下实例
class Person
{
public:
virtual void Buy()
{
cout << "原本买票--全价" << endl;
}
};
class Student:public Person
{
public:
virtual void Buy()
{
cout << "学生买票--半价"<<endl;
}
};
class Teacher:public Person
{
public:
virtual void Buy()
{
cout << "老师买票--优先" << endl;
}
};
void Pay(Person& p)
{
p.Buy();
}
int main()
{
Student s;
Teacher t;
Person p;
Pay(t);
Pay(p);
Pay(s);
return 0;
}
代码结果如下图:
C++11 override 和final
C++对虚函数的重写的要求是非常严格的。但是我们人有时候会因为疏忽的原因比如将函数名打错。这是我们的编译器并不会因此而报错。所以可能会因为这样的愿意而产生很难找到的bug。所以我们的C++11就增加了
override 这个关键字会帮你检查有没有实现重写,如果没有实现重写就报错。
使用案例如下:
class Car
{
public:
virtual void Drive(){}
};
class Benz :public Car
{
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
final 这个关键字会让你修饰的虚函数成为无法被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
重写、重载、重定义(隐藏)的区别
抽象类
抽象类其实就有纯虚函数函数的类。
纯虚函数:
在虚函数函数后面加上=0 即可
class T1
{
public:
virtual void test(int pos = 1) = 0;
};
这里我们T1中的test函数就是纯虚函数。拥有纯虚函数的类就是抽象类。
抽象类是无法直接定义对象的,只有纯虚函数重写才可以定义对象,但是我们的抽象类可以使用类型的指针和引用。所以我们可以用纯虚函数来编写需要重写的函数。
抽象类更能体现接口继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
这里我们来看看我们的A类对象虽然只有一个int _a的对象但是它的大小却是8。我们通过调试窗口观察一下。
可以看见我们多了一个vfptr 这个其实就是我们的虚函数表指针 。通过这个调试窗口可以看见_vfptr 指向的位置其实是一个指针,我们多创建几个虚函数试试。
可以看见我们的_vfptr 其实就是一个表,我们的test1的虚函数和test2虚函数的地址都会在这个表内存一份。
如果我们用子类来继承这样的让我们看看子类的虚函数表指针及虚函数表
可以当我们没有对虚函数进行重写的时候我们和父类公用了一个虚函数表。
此时我们重写一个test1来观察:
可以看出我们的test1在B中重写后B中就虚函数表中的test2位置就发生了改变。
所以这也可以说明我们的多态其实底层也是多创建了一个函数用于使用罢了。
不过为什么我们使用的父类的指针(引用)来接收的我们子类的类的时候却会调用我们子类的函数呢?
其实原因也很简单,我们的类在将子类传给父类的时候会先发生切片,但是这个切片会将我们的虚函数表指针同样给切下来,且 值不变。
所以我们的父类指针(引用)就可以由此来得到虚函数表。而如果我们没有使用指针(引用)的话我们直接使用父类的类来接收值的时候我们的虚表就不再是子类的值了。也就无法完成多态的实现。
而这种调用函数的方式又与我们之前的普通调用有所不同。
下图中func1()函数是虚函数而func2()不是虚函数。
可以看出我们的func1函数和func2函数调用有着明显的不同。
我们将func1这种通过变量值来调用函数的叫做运行时决议
而将func2这种直接调用的函数叫做编译时决议
我们的运行时决议的步骤要比我们的编译时决议多得多。
总结一下我们的派生类虚表的生成:1.先将基类中的虚表内容拷贝一份到派生类虚表中2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
这里我们要添加一个知识点: 我们的 虚函数和虚表存在哪里?答:虚函数和普通函数一样都在代码段中,而我们的虚表在VS中存在代码段中。
静态绑定和动态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,也成为静态多态,如:函数重载
- 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据集体拿到的类型确定程序的集体行为,调用具体的函数,也成为动态绑定。
结尾
再摆烂我就是狗🐕!!!!!!!!!!!!!!!
|