多态的学习与了解
目录
1.多态的概念
2.多态的实现
3.虚函数
虚函数的重写
题目练习
协变
析构函数的重写
重载重写隐藏
3.纯虚函数和抽象类
4.多态的原理
1.虚表
2.虚函数指针
总结:
1.多态的概念
说人话就是在继承的基础上传不同的对象实现不同的功能。
多态分为静态多态和动态多态,静态多态又称编译时多态,就是编译时就确定调用哪个函数,例如函数重载传不同的参数调用不同的函数这在编译时就是确定的,动态多态又称运行时多态,就是在运行时根据实际对象才决定调用哪个函数。
2.多态的实现
1.必须是基类的指针或引用调用的虚函数。
2.被调用的虚函数完成了重写。
下面代码打印的是B的fun()。就是满足了多态的两个实现条件。
那么我们要反问一句为什么要必须调用基类的指针或引用呢?赋值不行吗?答案是不行,因为前面继承中我们学习过派生类给基类赋值会发生切片,直接把派生类部分给切掉然后进行拷贝构造进行赋值,无法调用派生类f()。
我们现在记住一句话叫做是根据对象的实际类型进行多态函数的调用。我们传引用它的类型还是派生类只不过我们不能访问派生类的派生类新增部分,但是如果我们去传值进行拷贝构造的话,它的类型就是基类而不再是派生类。
class A {
public:virtual void f() {cout << "A::f()" << endl;}
};class B : public A
{
private:virtual void f() override {cout << "B::f()" << endl;}
};int main() {A* a = new B;a->f();return 0;
}
3.虚函数
下面我们来了解多态的实现的重要工具虚函数,在函数前面加virtual就是虚函数,它的作用就是来实现多态。
虚函数的重写
当派生类与父类的成员函数有同名时,该成员函数构成隐藏,当父类和父类的成员函数,它们的函数名相同,并且参数类型相同,返回值相同时,并且它们都是虚函数时啊,这时我们就构成了虚函数的重写。父类的虚函数是一个实现方式,子类的虚函数是一个实现方式,虚函数的复写是我们实现传不同对象调用不同函数的关键。
注意:正常情况下,我们父类和子类的虚函数都要加virtual,但是实际上,如果我们父类加虚函数关键字,子类不加,它仍然能够复写,但是如果父类没加虚函数关键字,但是子类加了,它就不能构成复写,这里是一个比较特殊的点。
题目练习
这个题目我们可以看出它是通过B类型的指针去调用test,B的test是从A中继承的,这时调用test又会调用func,但是它构成了多态所以我们调用B类的func,但是,这里重写仅仅是实现的重写,但是val仍然是父类的val。所以这道题选B,非常的坑。
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;p->test();
return 0;
}
协变
析构函数的重写
在继承中虚构函数要进行重写,我们要对析构函数进行谨慎处理,核心问题在于:
当通过 基类指针删除派生类对象 时,如果基类的析构函数 不是虚函数,会导致:
-
派生类的析构函数不被调用,仅调用基类的析构函数。
-
派生类独有的资源(如动态内存、文件句柄)泄漏。
下面我们用代码来解释一下。
如果我们不写析构函数的虚函数的话,它析构只会调用A的析构。核心问题再次强调一下:基类指针删除派生类对象,不写析构不会调派生类的析构。
using namespace std;class A {
public:virtual void f() {cout << "A::f()" << endl;}virtual ~A(){cout << "~A";}
};class B : public A
{
private:virtual void f() override {cout << "B::f()" << endl;}virtual ~B(){cout << "~B";}
};int main() {A* a = new A;A* b = new B;delete a;delete b;return 0;
}
了解完析构函数继承要写多态后,有的人就要问了,主播主播构造函数和静态函数需不需要写呢?
这个问题我们下面来解答会更清晰一点!
重载重写隐藏
重载是在同一作用域内,函数名相同参数不同的函数构成重载,根据不同的参数进行调用。
重写是在不同作用域父类和子类同名,同参数,同返回值的虚函数(函数名前加virtual)进行传不同的对象调用不同的函数实现多态。
隐藏是在不同作用域,父类和子类成员同名父类成员在派生类中会被隐藏。
3.纯虚函数和抽象类
在虚函数后加=0构成纯虚函数,它没有实际的定义。包含纯虚函数的类叫抽象类。抽象类不能实例化出对象,派生类如果继承后不重写纯虚函数也会是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写的话还是抽象类,无法实例化出对象。
了解完多态的定义和概念后,我们下来了解多态的原理。
4.多态的原理
多态是依赖虚表和虚指针来实现的。
1.虚表
每个包含虚函数的类都含有一个虚表,存储虚函数的地址。
当派生类继承含义虚函数的父类时,如果派生类不重写虚函数,它的虚表的内容和父类的虚表内容一致,只不过会新增派生类中新增的虚函数。如果派生类重写父类的虚函数,重写的虚函数就会覆盖原有的父类的虚函数。
注意:即使派生类中不重写父类的虚函数,它的vptr指向的物理地址也不一样。一个类共享一个vptr。
虚函数表细则:
2.虚函数指针
每个含有虚函数的类中有虚函数指针,指向虚函数表,我们通过这个指针来调用虚函数。
结合下图我们再结合我们继承的知识,我们来复盘一下虚函数调用的具体过程,__vfptr是指向虚表的指针,虚表里存储的虚函数的地址,如果我们父类含有虚函数,子类不对它进行重写,子类依旧会继承父类的虚函数并有自己独特的虚函数表,只不过它的定义和父类相同,如果我们对父类的虚函数进行重写,那么我们重写的虚函数会覆盖原有的虚函数。这样我们就可以调用重写的虚函数了。
当我们把派生类对象的引用或地址传给父类的引用或地址时,它不会发生切片,仅仅是我们不能通过它访问派生类新增的对象,但是__vfptr是可以访问的。且我们访问的是修改之后的__vfptr,我们借助代码再来理解一下。
“派生类对象会拥有自己的 __vfptr
,它默认指向派生类的虚表。如果派生类重写了基类的虚函数,则虚表中对应条目会更新为派生类的函数地址;否则保留基类的实现。
注意:这里的虚表都是独立的并不是基类的虚表和派生类的虚表共享一份,他们之间是独立的独立的独立的!!!
再借鉴下面这句话来理解一下,
这里就是虚函数的调用是根据其实际类型进行调用的。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier: public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main(){
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
总结:
相信到了这里大家就该大致了解多态和继承了,继承的基础上我们定义了多态来实现编程的方便,继承是父类的成员继承给派生类,但是它们之间的成员并没有关联,父类有int a,派生类继承int a,派生类中的a和父类的a实际是没有关系的,因为本质上它们是属于两个不同的类,多态我们引入了虚函数指针和虚函数表,虚函数表中的虚函数继承了父类的虚函数,如果我们再派生类中对他们进行重写,重写的虚函数就会覆盖原有的虚函数表中对应的虚函数。
附言:上文中我们提到了构造函数和静态函数不可以虚函数化,现在我们可以给出解释,构造函数不可以虚函数化是因为函数前面加virtual会调用虚函数指针,但是虚函数指针是构造中生成的,这会发生矛盾,编译无法通过,至于静态函数不可以虚函数化,静态函数不属于某个类的对象,它是属于整个类,它不通过对象调用,没有this指针,前面我们说过多态就是根据不同的对象调用来实现多态,但是静态函数不通过对象调用,所以我们的静态函数不可以虚函数化!!!
如有错误,请指正,博主多有感谢!!!