C++ 多态
多态的概念
**概念:**去执行某个行为,不同类型的对象去完成时会产生不同的状态(是通过对象实现的)
多态的定义和实现
多态的构成条件
基类虚函数和派生类虚函数构成重写(虚函数 + 函数名,返回值,参数类型相同)
必须由基类指针或引用去调用虚函数
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;} };
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
/父类虚函数和子类虚函数构成了重写,子类虚函数可以不加virtual,
/因为构成了多态,重写时是接口继承(父类虚函数声明继承到子类虚函数(属性,函数名,返回值,参数)),
/子类虚函数重写的是函数的实现,子类虚函数依旧保持虚函数属性
{
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;} };
析构函数的重写(基类与派生类析构函数的名字不同 )
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加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; }
例题:
以下程序输出结果是什么?
class A { public: virtual void func(int val = 1) { std::cout<<"A->"<<val<<std::endl; } virtual void test() { func(); } }; class B:public A { virtual void func(int val = 0) { std::cout<<"B->"<<val<<std::endl; } }; int main() { B*p = new B; P->test(); return 0; }
A: A->0 B: B->1 C: A->1 D: B->0
答案:B
解析:
p是子类指针,调用test()函数,test()函数是父类继承到子类中的,那么test()函数的第一个参数依旧是父类的指针,在调用test()函数时,传参是子类指针,发生赋值兼容的转换,在test()函数内,是一个指向子类对象的父类指针,这个指针调用func()函数,func()虚函数构成了重写,满足多态的条件,多态是通过对象调用的,就是调用子类的func()函数,因为构成了多态,是多态调用,func()虚函数是接口继承,子类func()函数中的val为1,所以答案是B->1
把这道题修改一下,输出结果是什么?:
int main() { B*p = new B; P->func(); return 0; }
答案:D
解析:通过子类指针调用这个重写函数,不符合多态条件,不是多态调用,是普通调用,答案为B->0
c++11 override final
final:修饰虚函数,表示该虚函数不能再被重写;修饰类,表示该类不能被继承
class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() //不能重写 { cout << "Benz-舒适" << endl; } };
class car final //car类不能被继承 { };
override(写在子类中):检查是否被重写,如果没有重写编译报错
class Car { public: virtual void Drive(){} }; class Benz :public Car { public: virtual void Drive() override { cout << "Benz-舒适" << endl; } };
重载,隐藏,重写总结:
抽象类
概念
包含纯虚函数的类叫作抽象类(也叫接口类),纯虚函数(在虚函数后面写上 = 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; } }; void Test() { //多态调用 Car* pBenz = new Benz; pBenz->Drive(); Car* pBMW = new BMW; pBMW->Drive(); }
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
虚函数表
先来观察一个包含虚函数类的结构
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; }; int main() { Base b; return 0; }
在b对象中,除了成员变量_b外,还包含一个指针,这个指针指向一块空间,这块空间存放虚函数的地址,这个指针叫做虚函数表指针,指向的这块空间是虚函数表(简称虚表),虚函数表中存放函数的地址。
题目:
// sizeof(Base)是多少? class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; };
sizeof(Base)是8,类Base中包含一个虚函数表指针和_b
/ 针对上面的代码我们做出以下改造 / 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; }
1、在派生类对象中,有两部分,一部分是父类继承下来的,虚函数表指针也在这一部分中,另一部分是子类自己的 2、子类的虚函数表,是先把父类的虚函数表拷贝到子类的虚函数表中,再把子类重写的虚函数,覆盖到子类的虚函数表中,这时虚函数表中存放的就是重写的虚函数的地址,虚函数表中的过程叫做覆盖,重写是语法的叫法,覆盖是原理层的叫法。派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的 3、虚函数表中只存放虚函数的地址,不存放普通函数,Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表 4、父类有一个虚函数表,所有父类的对象共用这一个虚函数表,子类有一个虚函数表,所有子类对象共用子类的虚函数表 5、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr 6、虚函数表中,存放的是虚函数的地址,不是虚函数,虚函数和普通函数一样都存放在代码段
多态的原理
我们使用一个代码具体讲解多态的原理
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 Mike; Func(Mike); //1 Student Johnson; Func(Johnson); //2 return 0; }
父类引用父类对象,通过虚表指针,找到虚表,通过存放的虚函数指针,调用父类的虚函数
使用父类引用接受子类对象,发生赋值兼容转换,发生切片,父类引用的是子类中继承自父类的那部分,这部分包含虚表指针,虚表指针直指向子类的虚表,通过虚表中的虚函数地址,调用子类的虚函数。
多态调用是运行时决议,因为父类指针或引用不知道接收的是那个对象,不能确定函数地址,所以在运行时确定
普通调用是编译时决议,如果函数调用和定义在一个文件中,在编译时,转换成汇编代码时,确定函数地址,在汇编时就确定了
如果调用和定义不在一个文件中,编译时无法确定函数地址,需要在生成符号表后,链接时,在符号表中寻找符号地址,所以在链接时确定
- 满足多态条件,多态调用(通过对象调用,父类指针指向什么对象,使用什么对象的虚函数),不满足多态条件,普通调用(什么指针,就使用什么的函数,也没有接口继承的事,是什么,就调什么,因为普通调用,是编译时决议)
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态