前言
本系列文章承接C语言的学习,需要有**++C语言的基础++** 才能学会哦~
第17篇主要讲的是有关于C++的++多态++ 。
C++已经进入进阶,加油!!
目录
多态
多态分为**++静态多态++和++动态多态++**
**静态多态:**传不同参数,编译出的函数就会不同(如函数重载、函数模板等)。
**动态多态:**同一个行为(函数),不同的对象完成的方式或者结果不一样(下文重点讲)。
语法
cpp
//函数重写Buyticket()
class Peraon
{
public:
virtual void BuyTicket() { cout << "买票------全价" << endl;}//①
};
class Student : public Person{
public:
virtual void BuyTicket() { cout << "买票------75折" << endl;}//②
};
void Func(Person* ptr)
{
ptr->Buyticket();
}
int main()
{
Person ps;
Student st;
//根据传入对象,调用指定的函数
Func(&ps);//调用①
Func(&st);//调用②
return 0;
}
实现多态的重要条件!!
① 必须是父类的指针或引用去调用虚函数。
父类指针既可以指向父类也可以指向子类。
② 被调用的函数必须是虚函数(关键字virtual在这里和虚继承的virtual没有关系)。
③ 子类必须对父类的虚函数进行重写。
重写(覆盖)的条件
同名、同参数、同返回值的虚函数,函数体不同,即可构成重写。如果++没有virtual修饰,就会被函数隐藏。++
++在满足多态的情况下,会调用子类重写的虚函数,但是函数声明遵循的是父类,因为继承了父类的整个函数签名++(连virtual也继承了)。
cpp
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()
{
B* p = new B;
p->test();
return 0;
}
观察上述代码,我们可以看到B的func满足了重写A的func的条件,函数声明的属性用的是父类的声明,但是调用的是子类的函数体,所以最后输出的结果是B->1,让val使用了父类的缺省值。
这里也告诉我们,在写多态的时候,父子类最好不要有不同的缺省值,不然就会有意外的事情发生。
协变(了解即可)
子类重写父类虚函数的时候,与父类虚函数的返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用,这就叫做协变。(算是特殊情况,了解即可)。
析构函数的重写
子类的++析构函数只要定义了,无论是否函数名是否相同,都可以与父类的析构函数构成重写++。++编译器会把子类的析构看成重写,并且进行特殊处理++。
特殊处理:把父类和子类的函数名在编译时都换为destructor()
override和final关键字
override放在函数的括号后面修饰。++被修饰的函数必须重写++ ,不然就报错,可用于自检。
final也放在函数的括号后面修饰,++被修饰的函数禁止重写++,不然就报错,可用于自检。
纯虚函数和抽象类
(如果懂java可以类别为java的抽象接口)
纯虚函数:在虚函数的后面写上=0,那么这个函数就叫做纯虚函数,纯虚函数不需要定义实现,就算实现了也必须被重写,所以只用声明即可。
抽象类
包含纯虚函数的类叫做抽象类。
抽象类不可以实例化出对象,如果其子类不对其纯虚函数进行重写,那么子类也是抽象类,也不能实例化出对象。
虚函数表
简称虚表,存储了抽象类中所有虚函数的指针。
++抽象类存储时,除了存储成员变量外,还会存储一个隐藏的虚函数表指针++,这个指针指向一个指针数组,存储了虚函数的指针。
cpp
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
如上的抽象类,运用内存对齐的知识,在32位下,该类的大小为12字节,而不是8字节,因为需要先存储虚函数表指针。

抽象类存储如上(_vftptr为虚函数表指针)
多态的原理
当抽象类被继承之后,其子类也会继承一个隐藏的成员------即虚函数表指针。但是指向的虚函数表不一样,子类的虚表指针指向的是自己的虚函数表。
因此,在进行多态调用的时候,++会在对应的虚表中找到对应的虚函数进行调用++。
cpp
void Func(Person* ptr)
{
//因为指针类型是Person*,所以传入的子类指针,只能访问父类成员的切片
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);//父类对象,调用父类对象的虚函数表中的函数
Func(&st);//子类对象,调用子类对象的虚函数表中的函数
}
如上,因为Func的参数为父类指针,
故:
① 子类调用的时候,++只能访问到父类成员的切片,具有安全性++;
② 同时因为父子类存储的虚表不同,++传入父类就调用父类的虚函数,传入子类就调用子类的虚函数,从而实现多态++(调用了同一个函数Func却有不同的效果)。
总结:指针指向谁,调用时就实现谁。
动态绑定和静态绑定
静态绑定: 对不满足多态条件的函数调用,++在编译的时候就绑定好++,编译时就已经确定了要调用的地址。
动态绑定: 对满足多态条件的函数调用,++在运行的时候才绑定++,也就是运行时到指定对象的虚函数表中查询要调用的地址来调用。
虚函数表原理
① 父类对象 的虚函数表中存放着父类所有虚函数的地址。
② 子类由继承的父类和自己的成员组成,子类的虚表在继承的父类成员里,但是和父类的虚表不是相同的。
③ 子类重写了父类的虚函数后,就会用重写后的虚函数覆盖虚函数表里的函数。没重写就不覆盖。
④ 子类的虚函数表包括父类的虚函数地址、重写父类的虚函数地址、自己的虚函数地址(但是vs监视窗口会隐藏)。
⑤同类型对象使用的虚函数表是一样的。如Person p1和Person p2使用的是同一个虚表,不会再另外开辟空间。
⑥一般虚表这个数组最后会放一个0x00000000进行标记(vs有放,g++没放,具体看编译器定义)。
⑦虚函数存放在代码段(也叫常量区)。在vs中,虚表也存放在代码段(虚表存放C++标准没要求)。
cpp
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void func1() { cout << "func1" << endl; }
void func2() { cout << "func2" << endl; }
int _a1 = 1;
int _a2 = 2;
};
class Student : public Person {
public:
virtual void Buyticket() { cout << "买票-打折" << endl; }
virtual void func1() { cout << "func1" << endl; }
virtual void func3() { cout << "func2" << endl; }
int _a3 = 3;
int _a4 = 4;
};
int main()
{
Person p;
Student st;
return 0;
}
运行后,启动监视窗口

可见++父子类对象的虚函数表地址是不一样的++,但是继承下来的_a1和_a2确实一样的。
补充:虽然函数名可以代表函数指针,但是取成员函数的地址要加取地址符号,这是语法规定,nowhy。
❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤